Merge branch 'lichang' into lite

This commit is contained in:
TsMask
2025-04-29 16:15:41 +08:00
86 changed files with 5724 additions and 1914 deletions

View File

@@ -12,19 +12,19 @@
"preview": "vite preview" "preview": "vite preview"
}, },
"dependencies": { "dependencies": {
"@ant-design/icons-vue": "^7.0.1", "@ant-design/icons-vue": "7.0.1",
"@antv/g6": "4.8.24", "@antv/g6": "4.8.25",
"@codemirror/lang-javascript": "6.2.3", "@codemirror/lang-javascript": "6.2.3",
"@codemirror/lang-yaml": "6.1.2", "@codemirror/lang-yaml": "6.1.2",
"@codemirror/merge": "6.10.0", "@codemirror/merge": "6.10.0",
"@codemirror/theme-one-dark": "6.1.2", "@codemirror/theme-one-dark": "6.1.2",
"@tato30/vue-pdf": "1.11.3", "@tato30/vue-pdf": "1.11.3",
"@vueuse/core": "12.8.2", "@vueuse/core": "13.0.0",
"@xterm/addon-fit": "0.10.0", "@xterm/addon-fit": "0.10.0",
"@xterm/xterm": "5.5.0", "@xterm/xterm": "5.5.0",
"ant-design-vue": "4.2.6", "ant-design-vue": "4.2.6",
"antdv-pro-layout": "4.2.0", "antdv-pro-layout": "4.2.0",
"antdv-pro-modal": "4.0.6", "antdv-pro-modal": "4.0.8",
"codemirror": "6.0.1", "codemirror": "6.0.1",
"crypto-js": "4.2.0", "crypto-js": "4.2.0",
"dayjs": "1.11.13", "dayjs": "1.11.13",
@@ -50,12 +50,12 @@
"@types/js-cookie": "3.0.6", "@types/js-cookie": "3.0.6",
"@types/node": "^18.0.0", "@types/node": "^18.0.0",
"@types/nprogress": "0.2.3", "@types/nprogress": "0.2.3",
"@vitejs/plugin-vue": "5.2.1", "@vitejs/plugin-vue": "5.2.3",
"less": "4.2.2", "less": "4.2.2",
"typescript": "5.6.3", "typescript": "5.8.2",
"unplugin-vue-components": "0.28.0", "unplugin-vue-components": "0.28.0",
"vite": "6.2.0", "vite": "6.3.3",
"vite-plugin-compression": "~0.5.1", "vite-plugin-compression": "0.5.1",
"vue-tsc": "2.2.0" "vue-tsc": "2.2.8"
} }
} }

View File

@@ -1,2 +1,2 @@
imsi,msisdn,sess_rules,pcc_rules,hdr_enrich,rfsp,sar,qos_audio,qos_video imsi,msisdn,sess_rules,pcc_rules,hdr_enrich,rfsp,sar,qos_audio,qos_video
001012082101039,1234,internet|ims_sig,internet|ims_sig,321321,255,321312,32131,32131 001012082101039,1234,internet|ims_sig,internet|ims_sig,321321,255,321312,32131,32131

View File

@@ -1,2 +1,2 @@
001011100001157,1234567890ABCDEF1234567890ABCDEF,0,8000,11111111111111111111111111111111 001011100001157,1234567890ABCDEF1234567890ABCDEF,0,8000,11111111111111111111111111111111
001011100001158,1234567890ABCDEF1234567890ABCDEF,0,8000,11111111111111111111111111111111 001011100001158,1234567890ABCDEF1234567890ABCDEF,0,8000,11111111111111111111111111111111

View File

@@ -1,2 +1,2 @@
001011100001157,62357000583,def_ambr,def_nssai,def_arfb,def_sar,0,3,def_snssai,1-000001&internet&ims,1,64,24,65,def_eps,1,010200000000,- 001011100001157,62357000583,def_ambr,def_nssai,def_arfb,def_sar,0,3,def_snssai,1-000001&internet&ims,1,64,24,65,def_eps,1,010200000000,-
001011100001158,62357000585,def_ambr,def_nssai,def_arfb,def_sar,0,3,def_snssai,1-000001&internet&ims,1,64,24,65,def_eps,1,010200000000,- 001011100001158,62357000585,def_ambr,def_nssai,def_arfb,def_sar,0,3,def_snssai,1-000001&internet&ims,1,64,24,65,def_eps,1,010200000000,-

View File

@@ -0,0 +1,3 @@
#username,password
62357000580,123456
62357000581,123456

View File

@@ -0,0 +1,4 @@
#vlote=0 MSISDN and IMSI need to be filled in the same way.
#imsi,msisdn,vlote,vni
460996650000580,62357000580,1,ims.mnc000.mcc460.3gppnetwork.org
62357000581,62357000581,0,ims.mnc000.mcc460.3gppnetwork.org

View File

@@ -1,15 +1,28 @@
import { CACHE_SESSION_CRYPTO_API } from '@/constants/cache-keys-constants';
import { sessionGet } from '@/utils/cache-session-utils';
import { request } from '@/plugins/http-fetch'; import { request } from '@/plugins/http-fetch';
// 登录方法 /**
*
* @param data
* @returns
*/
export function login(data: Record<string, string>) { export function login(data: Record<string, string>) {
return request({ return request({
url: '/login', url: '/auth/login',
method: 'POST', method: 'POST',
data: data, data: data,
whithToken: false, whithToken: false,
crypto: sessionGet(CACHE_SESSION_CRYPTO_API) !== 'false', });
}
/**
* 退
* @returns object
*/
export function logout() {
return request({
url: '/auth/logout',
method: 'POST',
repeatSubmit: false,
}); });
} }
@@ -20,11 +33,24 @@ export function login(data: Record<string, string>) {
*/ */
export function register(data: Record<string, any>) { export function register(data: Record<string, any>) {
return request({ return request({
url: '/register', url: '/auth/register',
method: 'POST', method: 'POST',
data: data, data: data,
whithToken: false, whithToken: false,
crypto: sessionGet(CACHE_SESSION_CRYPTO_API) !== 'false', });
}
/**
*
* @param data
* @returns
*/
export function refreshToken(refreshToken: string) {
return request({
url: '/auth/refresh-token',
method: 'POST',
data: { refreshToken },
whithToken: false,
}); });
} }
@@ -40,16 +66,15 @@ export function getInfo() {
} }
/** /**
* 退 *
* @returns object * @returns object
*/ */
export function logout() { export const getRouter = () => {
return request({ return request({
url: '/logout', url: '/router',
method: 'POST', method: 'GET',
repeatSubmit: false,
}); });
} };
/** /**
* *

View File

@@ -116,6 +116,21 @@ export function clearAlarm(ids: number[]) {
}); });
} }
/**
* 告警信息导出
* @param params 查询列表条件
* @returns object
*/
export function exportAlarm(params: Record<string, any>) {
return request({
url: '/neData/alarm/export',
method: 'GET',
params: params,
responseType: 'blob',
timeout: 60_000,
});
}
/** /**
* 手工同步 * 手工同步
* @param data 鉴权对象 * @param data 鉴权对象

View File

@@ -4,19 +4,6 @@ import { parseObjLineToHump } from '@/utils/parse-utils';
import { parseDateToStr } from '@/utils/date-utils'; import { parseDateToStr } from '@/utils/date-utils';
import useUserStore from '@/store/modules/user'; import useUserStore from '@/store/modules/user';
/**
* 查询列表
* @param query 查询参数
* @returns object
*/
export async function listAct(query: Record<string, any>) {
return await request({
url: `/neData/alarm/list`,
method: 'GET',
params: query,
});
}
/** /**
* 确认告警信息 * 确认告警信息
* @param data 鉴权对象 * @param data 鉴权对象

View File

@@ -1,65 +1,5 @@
import { RESULT_CODE_SUCCESS } from '@/constants/result-constants'; import { RESULT_CODE_SUCCESS } from '@/constants/result-constants';
import { request } from '@/plugins/http-fetch'; import { request } from '@/plugins/http-fetch';
import { parseDateToStr } from '@/utils/date-utils';
import { NE_TYPE_LIST } from '@/constants/ne-constants';
/**
* 查询公告列表
* @param query 查询参数
* @returns object
*/
export async function listMain() {
const result = await request({
url: '/api/rest/systemManagement/v1/elementType/all/objectType/systemState',
method: 'GET',
timeout: 60_000,
});
// console.log(result);
let realData = result.data.data;
const mergedData = realData.map((obj: any) => {
// console.log(obj);
const [key, value] = Object.entries(obj)[0];
const ipAddress = (value as any).ipAddress;
const systemState = (value as any).systemState;
const serialNum = (value as any).serialNum;
const version = (value as any).version;
const errCode = systemState && systemState['errorCode'];
var time = new Date();
// console.log(key, value);
let mergedObj;
if (errCode === undefined && systemState) {
mergedObj = {
...systemState,
refresh: parseDateToStr(time),
ipAddress: ipAddress,
name: key.split('/').join('_'),
status: 'Normal',
};
} else {
mergedObj = {
version,
refresh: parseDateToStr(time),
ipAddress,
serialNum,
name: key.split('/').join('_'),
expiryDate: '-',
status: 'Abnormal',
};
}
return mergedObj;
});
//通过sort进行冒泡排序
mergedData.sort((a: any, b: any) => {
const typeA = NE_TYPE_LIST.indexOf(a.name.split('_')[0]);
const typeB = NE_TYPE_LIST.indexOf(b.name.split('_')[0]);
if (typeA === -1) return 1; // 如果不在特定顺序中,排到后面
if (typeB === -1) return -1; // 如果不在特定顺序中,排到后面
return typeA - typeB;
});
return mergedData;
}
/** /**
* 获取服务器时间 * 获取服务器时间

View File

@@ -53,43 +53,3 @@ export function delFile(query: Record<string, any>) {
params: query, params: query,
}); });
} }
/**
* 更新FTP信息
* @param data 数据
* @returns object
*/
export function updateFTPInfo(data: Record<string, any>) {
return request({
url: `/lm/table/ftp`,
method: 'POST',
data: data,
crypto: sessionGet(CACHE_SESSION_CRYPTO_API) !== 'false',
});
}
/**
* 获取FTP信息
* @param data 数据
* @returns object
*/
export function getFTPInfo() {
return request({
url: `/lm/table/ftp`,
method: 'GET',
crypto: sessionGet(CACHE_SESSION_CRYPTO_API) !== 'false',
});
}
/**
* 发送FTP文件
* @param data 数据
* @returns object
*/
export function putFTPInfo(filePath: string, fileName: string) {
return request({
url: `/lm/table/ftp`,
method: 'PUT',
data: { filePath, fileName },
});
}

View File

@@ -83,43 +83,3 @@ export function importNeConfigBackup(data: Record<string, any>) {
data: data, data: data,
}); });
} }
/**
* 更新FTP信息
* @param data 数据
* @returns object
*/
export function updateFTPInfo(data: Record<string, any>) {
return request({
url: `/ne/config/backup/ftp`,
method: 'POST',
data: data,
crypto: sessionGet(CACHE_SESSION_CRYPTO_API) !== 'false',
});
}
/**
* 获取FTP信息
* @param data 数据
* @returns object
*/
export function getFTPInfo() {
return request({
url: `/ne/config/backup/ftp`,
method: 'GET',
crypto: sessionGet(CACHE_SESSION_CRYPTO_API) !== 'false',
});
}
/**
* 发送FTP文件
* @param data 数据
* @returns object
*/
export function putFTPInfo(path: string) {
return request({
url: `/ne/config/backup/ftp`,
method: 'PUT',
data: { path },
});
}

38
src/api/neData/backup.ts Normal file
View File

@@ -0,0 +1,38 @@
import { request } from '@/plugins/http-fetch';
/**
* 备份文件-获取FTP配置
* @returns object
*/
export function getBackupFTP() {
return request({
url: '/neData/backup/ftp',
method: 'GET',
});
}
/**
* 备份文件-文件FTP发送
* @param data 对象
* @returns object
*/
export function pushBackupFTP(data: Record<string, any>) {
return request({
url: '/neData/backup/ftp',
method: 'POST',
data,
});
}
/**
* 备份文件-更新FTP配置
* @param data 对象
* @returns object
*/
export function updateBackupFTP(data: Record<string, any>) {
return request({
url: '/neData/backup/ftp',
method: 'PUT',
data,
});
}

134
src/api/neData/udm_voip.ts Normal file
View File

@@ -0,0 +1,134 @@
import { request } from '@/plugins/http-fetch';
/**
* UDMVOIP用户重载数据
* @param neId 网元ID
* @returns object
*/
export function resetUDMVOIP(neId: string) {
return request({
url: `/neData/udm/voip/resetData/${neId}`,
method: 'PUT',
timeout: 180_000,
});
}
/**
* UDMVOIP用户列表
* @param query 查询参数
* @returns object
*/
export function listUDMVOIP(query: Record<string, any>) {
return request({
url: '/neData/udm/voip/list',
method: 'GET',
params: query,
timeout: 30_000,
});
}
/**
* UDMVOIP用户信息
* @param neId 网元ID
* @param username username
* @returns object
*/
export function getUDMVOIP(neId: string, username: string) {
return request({
url: `/neData/udm/voip/${neId}/${username}`,
method: 'GET',
});
}
/**
* UDMVOIP用户新增
* @param data VOIP对象
* @returns object
*/
export function addUDMVOIP(
neId: string,
data: { username: string; password: string }
) {
return request({
url: `/neData/udm/voip/${neId}`,
method: 'POST',
data: data,
timeout: 180_000,
});
}
/**
* UDMVOIP用户批量新增
* @param data VOIP对象
* @param num 数量
* @returns object
*/
export function batchAddUDMVOIP(
neId: string,
data: { username: string; password: string },
num: number
) {
return request({
url: `/neData/udm/voip/${neId}/${num}`,
method: 'POST',
data: data,
timeout: 180_000,
});
}
/**
* UDMVOIP用户删除
* @param data VOIP对象
* @returns object
*/
export function delUDMVOIP(neId: string, username: string) {
return request({
url: `/neData/udm/voip/${neId}/${username}`,
method: 'DELETE',
timeout: 180_000,
});
}
/**
* UDMVOIP用户批量删除
* @param neId 网元ID
* @param username username
* @param num 数量
* @returns object
*/
export function batchDelUDMVOIP(neId: string, username: string, num: number) {
return request({
url: `/neData/udm/voip/${neId}/${username}/${num}`,
method: 'DELETE',
timeout: 180_000,
});
}
/**
* UDMVOIP用户导出
* @param data 数据参数
* @returns bolb
*/
export function exportUDMVOIP(data: Record<string, any>) {
return request({
url: '/neData/udm/voip/export',
method: 'GET',
params: data,
responseType: 'blob',
timeout: 180_000,
});
}
/**
* UDMVOIP用户导入
* @param data 表单数据对象
* @returns object
*/
export function importUDMVOIP(data: Record<string, any>) {
return request({
url: `/neData/udm/voip/import`,
method: 'POST',
data,
timeout: 180_000,
});
}

View File

@@ -0,0 +1,127 @@
import { request } from '@/plugins/http-fetch';
/**
* UDMVolteIMS用户重载数据
* @param neId 网元ID
* @returns object
*/
export function resetUDMVolteIMS(neId: string) {
return request({
url: `/neData/udm/volte-ims/resetData/${neId}`,
method: 'PUT',
timeout: 180_000,
});
}
/**
* UDMVolteIMS用户列表
* @param query 查询参数
* @returns object
*/
export function listUDMVolteIMS(query: Record<string, any>) {
return request({
url: '/neData/udm/volte-ims/list',
method: 'GET',
params: query,
timeout: 30_000,
});
}
/**
* UDMVolteIMS用户信息
* @param neId 网元ID
* @param imsi IMSI
* @returns object
*/
export function getUDMVolteIMS(neId: string, imsi: string) {
return request({
url: `/neData/udm/volte-ims/${neId}/${imsi}`,
method: 'GET',
});
}
/**
* UDMVolteIMS用户新增
* @param data 签约对象
* @returns object
*/
export function addUDMVolteIMS(data: Record<string, any>) {
return request({
url: `/neData/udm/volte-ims/${data.neId}`,
method: 'POST',
data: data,
timeout: 180_000,
});
}
/**
* UDMVolteIMS用户批量新增
* @param data 签约对象
* @param num 数量
* @returns object
*/
export function batchAddUDMVolteIMS(data: Record<string, any>, num: number) {
return request({
url: `/neData/udm/volte-ims/${data.neId}/${num}`,
method: 'POST',
data: data,
timeout: 180_000,
});
}
/**
* UDMVolteIMS用户删除
* @param data 签约对象
* @returns object
*/
export function delUDMVolteIMS(neId: string, imsi: string) {
return request({
url: `/neData/udm/volte-ims/${neId}/${imsi}`,
method: 'DELETE',
timeout: 180_000,
});
}
/**
* UDMVolteIMS用户批量删除
* @param neId 网元ID
* @param imsi IMSI
* @param num 数量
* @returns object
*/
export function batchDelUDMVolteIMS(neId: string, imsi: string, num: number) {
return request({
url: `/neData/udm/volte-ims/${neId}/${imsi}/${num}`,
method: 'DELETE',
timeout: 180_000,
});
}
/**
* UDMVolteIMS用户导出
* @param data 数据参数
* @returns bolb
*/
export function exportUDMVolteIMS(data: Record<string, any>) {
return request({
url: '/neData/udm/volte-ims/export',
method: 'GET',
params: data,
responseType: 'blob',
timeout: 180_000,
});
}
/**
* UDMVolteIMS用户导入
* @param data 表单数据对象
* @returns object
*/
export function importUDMVolteIMS(data: Record<string, any>) {
return request({
url: `/neData/udm/volte-ims/import`,
method: 'POST',
data,
timeout: 180_000,
});
}

View File

@@ -220,6 +220,6 @@ export function taskRun(data: Record<string, any>) {
export function taskStop(data: Record<string, any>) { export function taskStop(data: Record<string, any>) {
return request({ return request({
url: `/api/rest/performanceManagement/v1/elementType/${data.neType.toLowerCase()}/objectType/measureTask?id=${data.id}`, url: `/api/rest/performanceManagement/v1/elementType/${data.neType.toLowerCase()}/objectType/measureTask?id=${data.id}`,
method: 'PATCH', method: 'PUT',
}); });
} }

View File

@@ -1,12 +0,0 @@
import { request } from '@/plugins/http-fetch';
/**
* 获取路由
* @returns object
*/
export const getRouters = () => {
return request({
url: '/router',
method: 'GET',
});
};

View File

@@ -136,3 +136,16 @@ export function changeUserStatus(
data: { userId, statusFlag }, data: { userId, statusFlag },
}); });
} }
/**
* 用户强制重置密码
* @param password 密码
* @returns object
*/
export function updateUserPasswordForce(password: string) {
return request({
url: '/system/user/profile/password-force',
method: 'PUT',
data: { password },
});
}

View File

@@ -210,6 +210,50 @@ export function chunkUpload(data: FormData) {
}); });
} }
/**
* 本地文件列表
* @param path 文件路径
* @param search search prefix
* @returns object
*/
export async function listFile(query: Record<string, any>) {
return request({
url: `/file/list`,
method: 'GET',
params: query,
});
}
/**
* 本地文件获取下载
* @param path 文件路径
* @param fileName 文件名
* @returns object
*/
export async function getFile(path: string, fileName: string) {
return request({
url: `/file`,
method: 'GET',
params: { path, fileName },
responseType: 'blob',
timeout: 60_000,
});
}
/**
* 本地文件删除
* @param path 文件路径
* @param fileName 文件名
* @returns object
*/
export async function delFile(path: string, fileName: string) {
return request({
url: `/file`,
method: 'DELETE',
params: { path, fileName },
});
}
/** /**
* 转存上传文件到静态资源 * 转存上传文件到静态资源
* @returns object * @returns object

View File

@@ -1,27 +0,0 @@
import { request } from '@/plugins/http-fetch';
/**
* 跟踪任务数据列表
* @param query 查询参数
* @returns object
*/
export async function listTraceData(query: Record<string, any>) {
return request({
url: '/trace/task/list',
method: 'GET',
params: query,
});
}
/**
* 信令数据解析HTML
* @param id 任务ID
* @returns
*/
export function getTraceRawInfo(id: Record<string, string>) {
return request({
url: `/api/rest/traceManagement/v1/decMessage/${id}`,
method: 'GET',
responseType: 'text',
});
}

View File

@@ -62,3 +62,18 @@ export function packetKeep(taskNo: string, duration: number = 120) {
data: { taskNo, duration }, data: { taskNo, duration },
}); });
} }
/**
* 信令跟踪文件
* @param taskNo 对象
* @returns object
*/
export function packetPCAPFile(taskNo: string) {
return request({
url: '/trace/packet/filePull',
method: 'GET',
params: { taskNo },
responseType: 'blob',
timeout: 180_000,
});
}

View File

@@ -76,29 +76,31 @@ export function filePullTask(traceId: string) {
method: 'GET', method: 'GET',
params: { traceId }, params: { traceId },
responseType: 'blob', responseType: 'blob',
timeout: 60_000, timeout: 180_000,
}); });
} }
/** /**
* 获取网元跟踪接口列表 * 跟踪任务数据列表
* @param query 查询参数
* @returns object * @returns object
*/ */
export async function getNeTraceInterfaceAll() { export async function listTraceData(query: Record<string, any>) {
// 发起请求 return request({
const result = await request({ url: '/trace/data/list',
url: `/api/rest/databaseManagement/v1/elementType/omc_db/objectType/ne_info`, method: 'GET',
params: query,
});
}
/**
* 查询跟踪任务数据信息
* @param id ID
* @returns object
*/
export async function getTraceData(id: string | number) {
return request({
url: `/trace/data/${id}`,
method: 'GET', method: 'GET',
params: {
SQL: `SELECT ne_type,interface FROM trace_info GROUP BY ne_type,interface`,
},
}); });
// 解析数据
if (result.code === RESULT_CODE_SUCCESS && Array.isArray(result.data.data)) {
let data = result.data.data[0];
return Object.assign(result, {
data: parseObjLineToHump(data['trace_info']),
});
}
return result;
} }

View File

@@ -0,0 +1,218 @@
<script setup lang="ts">
import { onMounted, onUnmounted, reactive, ref } from 'vue';
import { regExpPasswd, regExpUserName } from '@/utils/regular-utils';
import { Form, message, Modal } from 'ant-design-vue';
import useI18n from '@/hooks/useI18n';
import useUserStore from '@/store/modules/user';
import { updateUserPasswordForce } from '@/api/system/user';
import { RESULT_CODE_SUCCESS } from '@/constants/result-constants';
import { getConfigKey } from '@/api/system/config';
const userStore = useUserStore();
const { t } = useI18n();
/**对话框对象信息状态类型 */
type ModalStateType = {
/**重置密码框是否显示 */
open: boolean;
/**标题 */
title: string;
/**表单数据 */
from: Record<string, any>;
/**确定按钮 loading */
confirmLoading: boolean;
/**密码策略 */
passwordPolicy: Record<string, any>;
/**密码有效期 */
passwdExpireEnable: boolean;
passwdExpire: Record<string, any>;
};
/**对话框对象信息状态 */
let modalState: ModalStateType = reactive({
open: userStore.forcePasswdChange,
title: t('components.ForcePasswdChange.title'),
from: {
userId: userStore.userId,
userName: userStore.userName,
password: '',
},
confirmLoading: false,
passwordPolicy: { minLength: 8, specialChars: 2, uppercase: 1, lowercase: 1 },
passwdExpireEnable: false,
passwdExpire: { expHours: 2, alertHours: 1 },
});
/**对话框内表单属性和校验规则 */
const modalStateFrom = Form.useForm(
modalState.from,
reactive({
userName: [
{
required: true,
pattern: regExpUserName,
message: t('views.system.user.userNameTip'),
},
],
password: [
{
required: true,
pattern: regExpPasswd,
message: t('views.system.user.passwdTip'),
},
],
})
);
/**对话框提交确认 */
function fnModalOk() {
const { password } = modalState.from;
if (!password) {
message.error({
content: t('views.system.user.passwdTip'),
duration: 2,
});
return;
}
updateUserPasswordForce(password).then(res => {
if (res.code === RESULT_CODE_SUCCESS) {
userStore.fnLogOut();
modalState.confirmLoading = true;
Modal.success({
title: t('common.tipTitle'),
content: t('views.account.settings.submitOkTip', {
num: modalState.from.userName,
}),
okText: t('views.account.settings.submitOk'),
onOk() {
window.location.reload();
},
});
} else {
message.error({
content: `${res.msg}`,
duration: 2,
});
}
});
}
/**组件实例挂载之后调用 */
onMounted(() => {
Promise.all([
getConfigKey('sys.user.passwordPolicy'),
getConfigKey('sys.user.passwdExpire'),
]).then(resArr => {
if (resArr[0].code === RESULT_CODE_SUCCESS) {
try {
modalState.passwordPolicy = JSON.parse(resArr[0].data);
} catch (error) {
console.error('passwordPolicy', error);
}
}
if (resArr[1].code === RESULT_CODE_SUCCESS) {
try {
const data = JSON.parse(resArr[1].data);
if (data.expHours % 24 === 0) {
data.expHours = data.expHours / 24;
} else {
data.expHours = (data.expHours / 24).toFixed(2);
}
if (data.alertHours % 24 === 0) {
data.alertHours = data.alertHours / 24;
} else {
data.alertHours = (data.alertHours / 24).toFixed(2);
}
modalState.passwdExpire = data;
modalState.passwdExpireEnable = data.expHours > 0;
} catch (error) {
console.error('passwdExpire', error);
}
}
});
});
/**组件实例被卸载之后调用 */
onUnmounted(() => {});
</script>
<template>
<a-modal
v-model:open="modalState.open"
get-container="#app"
:footer="null"
:zIndex="1008"
:closable="false"
:keyboard="false"
:destroyOnClose="true"
:mask-closable="false"
:title="modalState.title"
>
<a-form
name="modalStateFromByResetPwd"
layout="horizontal"
:label-col="{ span: 6 }"
:label-wrap="true"
>
<a-form-item
:label="t('views.system.user.account')"
name="userName"
v-bind="modalStateFrom.validateInfos.userName"
>
<a-input :value="modalState.from.userName" disabled :maxlength="30">
<template #prefix>
<UserOutlined />
</template>
</a-input>
</a-form-item>
<a-form-item
:label="t('views.system.user.loginPwd')"
name="password"
v-bind="modalStateFrom.validateInfos.password"
>
<a-input-password
v-model:value="modalState.from.password"
:disabled="modalState.confirmLoading"
:maxlength="26"
>
<template #prefix>
<LockOutlined />
</template>
</a-input-password>
</a-form-item>
<a-form-item name="ok" :wrapper-col="{ offset: 6 }">
<a-button
type="primary"
@click="fnModalOk()"
:disabled="modalState.confirmLoading"
>
{{ t('common.ok') }}
</a-button>
</a-form-item>
<!-- 提示信息 -->
<a-form-item name="info" :label="t('components.ForcePasswdChange.desc')">
<div>
<p>
{{ t('components.ForcePasswdChange.passwordPolicy') }}<br />
{{
t(
'components.ForcePasswdChange.passwordPolicyMsg',
modalState.passwordPolicy
)
}}
</p>
<p v-if="modalState.passwdExpireEnable">
{{ t('components.ForcePasswdChange.passwdExpire') }}<br />
{{
t('components.ForcePasswdChange.passwdExpireMsg', {
expDay: modalState.passwdExpire.expHours,
alertDay: modalState.passwdExpire.alertHours,
})
}}
</p>
</div>
</a-form-item>
</a-form>
</a-modal>
</template>
<style lang="less" scoped></style>

View File

@@ -1,14 +1,14 @@
/**响应-code加密数据 */ /**响应-code加密数据 */
export const RESULT_CODE_ENCRYPT = 2; export const RESULT_CODE_ENCRYPT = 200999;
/**响应-msg加密数据 */ /**响应-msg加密数据 */
export const RESULT_MSG_ENCRYPT: Record<string, string> = { export const RESULT_MSG_ENCRYPT: Record<string, string> = {
zh_CN: '加密!', zh_CN: '加密!',
en_US: 'encrypt!', en_US: 'Encrypt!',
}; };
/**响应-code正常成功 */ /**响应-code正常成功 */
export const RESULT_CODE_SUCCESS = 1; export const RESULT_CODE_SUCCESS = 200001;
/**响应-msg正常成功 */ /**响应-msg正常成功 */
export const RESULT_MSG_SUCCESS: Record<string, string> = { export const RESULT_MSG_SUCCESS: Record<string, string> = {
@@ -17,7 +17,16 @@ export const RESULT_MSG_SUCCESS: Record<string, string> = {
}; };
/**响应-code错误失败 */ /**响应-code错误失败 */
export const RESULT_CODE_ERROR = 0; export const RESULT_CODE_ERROR = 400001;
/**响应-code错误异常 */
export const RESULT_CODE_EXCEPTION = 500001;
/**响应-服务器连接出错 */
export const RESULT_MSG_SERVER_ERROR: Record<string, string> = {
zh_CN: '服务器连接出错!',
en_US: 'Server Connection Error!',
};
/**响应-msg错误失败 */ /**响应-msg错误失败 */
export const RESULT_MSG_ERROR: Record<string, string> = { export const RESULT_MSG_ERROR: Record<string, string> = {
@@ -37,18 +46,6 @@ export const RESULT_MSG_NOT_TYPE: Record<string, string> = {
en_US: 'Unknown Response Data Type!', en_US: 'Unknown Response Data Type!',
}; };
/**响应-服务器连接出错 */
export const RESULT_MSG_SERVER_ERROR: Record<string, string> = {
zh_CN: '服务器连接出错!',
en_US: 'Server Connection Error!',
};
/**响应-请求地址未找到 */
export const RESULT_MSG_URL_NOTFOUND: Record<string, string> = {
zh_CN: '请求地址未找到!',
en_US: 'Request Address Not Found!',
};
/**响应-数据正在处理,请勿重复提交 */ /**响应-数据正在处理,请勿重复提交 */
export const RESULT_MSG_URL_RESUBMIT: Record<string, string> = { export const RESULT_MSG_URL_RESUBMIT: Record<string, string> = {
zh_CN: '数据正在处理,请勿重复提交!', zh_CN: '数据正在处理,请勿重复提交!',

View File

@@ -2,10 +2,13 @@
export const TOKEN_RESPONSE_FIELD = 'accessToken'; export const TOKEN_RESPONSE_FIELD = 'accessToken';
/**令牌-请求头标识前缀 */ /**令牌-请求头标识前缀 */
export const TOKEN_KEY_PREFIX = 'Bearer '; export const TOKEN_KEY_PREFIX = 'Bearer';
/**令牌-请求头标识 */ /**令牌-请求头标识 */
export const TOKEN_KEY = 'Authorization'; export const TOKEN_KEY = 'Authorization';
/**令牌-存放Cookie标识 */ /**令牌-访问令牌存放Cookie标识 */
export const TOKEN_COOKIE = 'AuthOMC'; export const TOKEN_ACCESS_COOKIE = 'omc_access';
/**令牌-刷新令牌存放Cookie标识 */
export const TOKEN_REFRESH_COOKIE = 'omc_refresh';

View File

@@ -16,7 +16,6 @@ export default {
errorFields: 'Please fill in the required information in {num} correctly!', errorFields: 'Please fill in the required information in {num} correctly!',
tablePaginationTotal: 'Total {total} items', tablePaginationTotal: 'Total {total} items',
noData: "No Data", noData: "No Data",
zebra:'Tabular zebra pattern',
ok: 'Ok', ok: 'Ok',
cancel: 'Cancel', cancel: 'Cancel',
close: 'Close', close: 'Close',
@@ -131,7 +130,7 @@ export default {
}, },
LockScreen: { LockScreen: {
inputPlacePwd:'Lock Screen Password', inputPlacePwd:'Lock Screen Password',
validSucc:'Validation Passed', enter:'Enter',
validError:'Validation Failure', validError:'Validation Failure',
backLogin:'Logout to Relogin', backLogin:'Logout to Relogin',
backReload:'Restarting now, please wait...', backReload:'Restarting now, please wait...',
@@ -139,6 +138,14 @@ export default {
systemReset:'Resetting now, please wait...', systemReset:'Resetting now, please wait...',
systemReset2:'Data information is being reset.', systemReset2:'Data information is being reset.',
}, },
ForcePasswdChange: {
title: 'Password Change',
desc: 'Instruction',
passwordPolicy: 'Password policy strength',
passwordPolicyMsg: 'At least {minLength} bits, containing at least {specialChars} special characters and at least {uppercase} uppercase and at least {lowercase} lowercase letters.',
passwdExpire: 'Password expiration date',
passwdExpireMsg: 'Valid for {expDay} days, please change your password {alertDay} days before expiration.',
},
}, },
// 静态路由 // 静态路由
@@ -163,7 +170,7 @@ export default {
userNameReg: 'The account cannot start with a number and can contain uppercase and lowercase letters, numbers, and no less than 5 digits.', userNameReg: 'The account cannot start with a number and can contain uppercase and lowercase letters, numbers, and no less than 5 digits.',
userNamePlease: 'Please enter the correct login account', userNamePlease: 'Please enter the correct login account',
userNameHit: 'Login account', userNameHit: 'Login account',
passwordReg: 'The password should contain at least uppercase and lowercase letters, numbers, special symbols, and no less than 6 digits.', passwordReg: 'Please enter the correct password format',
passwordPlease: 'Please enter the correct login password', passwordPlease: 'Please enter the correct login password',
passwordHit: 'Login password', passwordHit: 'Login password',
passwordConfirmHit: 'Confirm login password', passwordConfirmHit: 'Confirm login password',
@@ -295,7 +302,7 @@ export default {
oldPasswordTip: "The old password must not be empty and must be at least 6 digits long", oldPasswordTip: "The old password must not be empty and must be at least 6 digits long",
oldPasswordPleace: "Please enter the old password", oldPasswordPleace: "Please enter the old password",
newPassword: "New Password", newPassword: "New Password",
newPasswordTip: "Password contains at least upper and lower case letters, numbers, special symbols, and not less than 6 digits", newPasswordTip: "Please enter the correct password format",
newPassworddPleace: "Please enter a new password", newPassworddPleace: "Please enter a new password",
confirmPassword: "Confirm new password", confirmPassword: "Confirm new password",
confirmPasswordPleace: "Please confirm the new password", confirmPasswordPleace: "Please confirm the new password",
@@ -506,7 +513,7 @@ export default {
delTip: 'Confirm deletion of network element information data items?', delTip: 'Confirm deletion of network element information data items?',
oam: { oam: {
title: 'OAM Configuration', title: 'OAM Configuration',
sync: 'Sync to NE', restart: 'Restart NE',
oamEnable: 'Service', oamEnable: 'Service',
oamPort: 'Port', oamPort: 'Port',
snmpEnable: 'Service', snmpEnable: 'Service',
@@ -659,6 +666,19 @@ export default {
name: "Name", name: "Name",
downTip: 'Confirmed to download the backup file [{txt}]?', downTip: 'Confirmed to download the backup file [{txt}]?',
title: "Modify Backup {txt}", title: "Modify Backup {txt}",
backupModal: {
pushFileOper: "Send Current File To Remote Backup",
title: "Setting Remote Backup Service",
enable: "Enable",
toIp: "Service IP",
toIpPleace: "Please input the remote backup server IP address",
toPort: "Service Port",
username: "UserName",
usernamePleace: 'Please enter the service login username',
password: "Password",
dir: "Save Dir",
dirPleace: 'Please enter the service address target file directory',
}
}, },
neQuickSetup: { neQuickSetup: {
reloadPara5G: 'Reload', reloadPara5G: 'Reload',
@@ -711,8 +731,45 @@ export default {
}, },
neData: { neData: {
common: { common: {
startIMSI: 'Starting IMSI',
imsi: 'IMSI',
imsiTip: 'IMSI=MCC+MNC+MSIN',
imsiTip1: 'MCC=Mobile Country Code, consisting of three digits.',
imsiTip2: 'MNC = Mobile Network Number, consisting of two digits',
imsiTip3: 'MSIN = Mobile Subscriber Identification Number, consisting of 10 equal digits.',
imsiPlease: "Please enter IMSI correctly",
msisdn: 'Mobile Customer Identification Number',
msisdnPlease: "Please enter the Mobile Customer Identification Number correctly",
loadDataConfirm: 'Confirmed to reload data?',
loadData: 'Load Data',
loadDataTip: 'Successfully fetched loaded data: {num} items, the system is internally updating the data, it will take about {timer} seconds, please wait!!!!!.',
batchOper: 'Batch Operation',
batchAddText: 'Batch Addition',
batchDelText: 'Batch Deletion',
batchUpdateText: 'Batch Update',
batchNum: 'Number of releases',
checkDel:'Check Delete',
importTemplate: 'Download Template', importTemplate: 'Download Template',
}, },
udmVOIP: {
startUsername: 'Starting username',
username: 'username',
usernamePlease: "Please enter your username correctly",
password: "password",
passwordPlease: "Please enter your password correctly",
addTitle: 'Add VOIP subscriber',
delTip: 'Confirm that you want to delete the information of VOIP user as [{num}]?',
exportTip: "Confirm exporting xlsx table files based on search criteria?",
},
udmVolteIMS: {
startMSISDN: 'Starting MSISDN',
voipTip: 'When VoIP is selected MSISDN will be equal to IMSI',
addTitle: 'Addition of new IMS subscribers',
vniTip: 'Example: ims.mnc000.mcc000.3gppnetwork.org',
vniPlease: 'Please enter VNI correctly',
delTip: 'Are you sure you want to delete the information of IMS signing as [{num}]?',
exportTip: "Confirm exporting xlsx table files based on search criteria?",
},
baseStation: { baseStation: {
list: "List", list: "List",
topology: "Topology", topology: "Topology",
@@ -735,6 +792,12 @@ export default {
exportTip: "Confirm exporting xlsx table files based on search criteria?", exportTip: "Confirm exporting xlsx table files based on search criteria?",
importDataEmpty: "Imported data is empty", importDataEmpty: "Imported data is empty",
}, },
backupData: {
auth: "UDM Authentication",
sub: "UDM Subscribers",
voip: "VoIP Authentication",
volte: "IMS Subscribers",
}
}, },
neUser: { neUser: {
auth: { auth: {
@@ -1004,25 +1067,6 @@ export default {
}, },
}, },
traceManage: { traceManage: {
analysis: {
imsi: 'IMSI',
imsiPlease: 'Please enter IMSI',
msisdn: 'MSISDN',
msisdnPlease: 'Please enter MSISDN',
trackTaskId: 'Task ID',
srcIp: 'Source IP Address',
dstIp: 'Destination IP Address',
signalType: 'Signaling Type',
msgDirect: 'Message Direction',
msgType: 'Message Type',
rowTime: 'Record Time',
signalData: 'Signaling Data',
signalDetail: 'Signaling Details',
noData: 'No information content',
taskTitle: 'Task {num}',
taskDownText: 'Download HTML',
taskDownTip: 'Confirm downloading the signaling details HTML file?',
},
pcap: { pcap: {
capArgPlease: 'Please enter tcpdump -i any support parameter', capArgPlease: 'Please enter tcpdump -i any support parameter',
cmd: 'Command', cmd: 'Command',
@@ -1073,30 +1117,35 @@ export default {
imsiTip: 'Mobile communication IMSI number', imsiTip: 'Mobile communication IMSI number',
srcIp: 'Source IP Address', srcIp: 'Source IP Address',
srcIpPlease: 'Please enter the IP address', srcIpPlease: 'Please enter the IP address',
srcIpTip: 'Current sender IPv4 address', srcIpTip: 'sending IPv4 address',
dstIp: 'Destination IP Address', dstIp: 'Destination IP Address',
dstIpPlease: 'Please enter the IP address', dstIpPlease: 'Please enter the IP address',
dstIpTip: 'IPv4 address of the receiving end of the other party', dstIpTip: 'receiving end IPv4 address',
interfaces: 'Signaling Interface', interfaces: 'Signaling Interface',
interfacesPlease: 'Please enter the signaling interface', interfacesPlease: 'Please enter the signaling interface',
signalPort: 'Signal Port', rangePicker: 'Task Time',
signalPortPlease: 'Please enter the signaling port',
signalPortTip: 'Port of the side corresponding to the destination IP address or source IP address',
rangePicker: 'Start/End Time',
rangePickerPlease: 'Please select the start and end time of the task', rangePickerPlease: 'Please select the start and end time of the task',
remark: 'Remark', remark: 'Remark',
remarkPlease: 'Task description can be entered', remarkPlease: 'Task description can be entered',
addTask: 'Add Task', addTask: 'Add Task',
editTask: 'Modify Task',
viewTask: 'View Task', viewTask: 'View Task',
errorTaskInfo: 'Failed to obtain task information', errorTaskInfo: 'Failed to obtain task information',
delTaskTip: 'Are you sure to delete the data item with record ID {id} ?', delTaskTip: 'Are you sure to delete the data item with record ID {id} ?',
stopTask: 'Successful cessation of tasks {id}', stopTask: 'Successful cessation of tasks {id}',
stopTaskTip: 'Confirm stopping the task with record ID {id} ?', stopTaskTip: 'Confirm stopping the task with record ID {id} ?',
pcapView: "Tracking Data Analysis", pcapView: "Track Data Analysis",
traceFile: "Tracking File", traceFile: "Track File",
errMsg: "Error Message", errMsg: "Error Message",
imsiORmsisdn: "imsi or msisdn is null, cannot start task", imsiORmsisdn: "imsi or msisdn is null, cannot start task",
dataView: "Track Data",
protocolOrInterface: "Protocol/Interface",
msgNe: 'Network Element',
msgEvent: 'Event',
msgType: 'Type',
msgDirect: 'Direction',
msgLen: 'Length',
rowTime: 'Time',
taskInfo: 'Task information',
}, },
}, },
faultManage: { faultManage: {
@@ -1141,13 +1190,11 @@ export default {
delSuss:'Clear successfully', delSuss:'Clear successfully',
delSure:'Whether to clear this alarm', delSure:'Whether to clear this alarm',
showSet:'Show filter settings', showSet:'Show filter settings',
exportTip: "Confirm exporting xlsx table files based on search criteria?",
exportSure:'Confirm whether to export all active alarm information', exportSure:'Confirm whether to export all active alarm information',
viewIdInfo:'View {alarmId} record information', viewIdInfo:'View {alarmId} record information',
closeModal:'Close', closeModal:'Close',
}, },
historyAlarm:{
exportSure:'Confirm whether to export all historical alarm information',
},
faultSetting:{ faultSetting:{
interfaceType:'Type', interfaceType:'Type',
email:'Email', email:'Email',
@@ -1216,12 +1263,17 @@ export default {
tailLines: 'End Lines', tailLines: 'End Lines',
}, },
exportFile:{ exportFile:{
fileName:'File Source', fileSource:'File Source',
fileSourcePlease:'Please select the source of the document',
downTip: "Confirm the download file name is [{fileName}] File?", downTip: "Confirm the download file name is [{fileName}] File?",
downTipErr: "Failed to get file", downTipErr: "Failed to get file",
deleteTip: "Confirm the delete file name is [{fileName}] File?", deleteTip: "Confirm the delete file name is [{fileName}] File?",
deleteTipErr: "Failed to delete file", deleteTipErr: "Failed to delete file",
selectTip:"Please select File Name", operateLog:'Operation Log',
cdrIMS:'Voice CDR',
cdrSMF:'Data CDR',
cdrSMSC:'SMS CDR',
cdrSGWC:'Roaming Data CDR',
} }
}, },
monitor: { monitor: {
@@ -1599,7 +1651,7 @@ export default {
loginTime: 'Login Time', loginTime: 'Login Time',
status: 'Status', status: 'Status',
userNameTip:'The account number can only contain strings of uppercase letters, lowercase letters and numbers with a minimum length of 6 digits', userNameTip:'The account number can only contain strings of uppercase letters, lowercase letters and numbers with a minimum length of 6 digits',
passwdTip:'The password should contain at least uppercase and lowercase letters, numbers, special symbols, and no less than 6 digits', passwdTip:'Please enter the correct password format',
nickNameTip:'Nicknames no less than 2 digits', nickNameTip:'Nicknames no less than 2 digits',
emailTip:'Please enter the correct email address', emailTip:'Please enter the correct email address',
phoneTip:'Please enter the correct phone number', phoneTip:'Please enter the correct phone number',

View File

@@ -16,7 +16,6 @@ export default {
errorFields: '请正确填写 {num} 处必填信息!', errorFields: '请正确填写 {num} 处必填信息!',
tablePaginationTotal: '总共 {total} 条', tablePaginationTotal: '总共 {total} 条',
noData: "暂无数据", noData: "暂无数据",
zebra:'表格斑马纹',
ok: '确定', ok: '确定',
cancel: '取消', cancel: '取消',
close: '关闭', close: '关闭',
@@ -131,7 +130,7 @@ export default {
}, },
LockScreen: { LockScreen: {
inputPlacePwd:'请输入锁屏密码', inputPlacePwd:'请输入锁屏密码',
validSucc:'校验通过', enter:'进入',
validError:'校验失败', validError:'校验失败',
backLogin:'退出并重新登录', backLogin:'退出并重新登录',
backReload:'正在重启,请稍等...', backReload:'正在重启,请稍等...',
@@ -139,6 +138,14 @@ export default {
systemReset:'正在重置,请稍等...', systemReset:'正在重置,请稍等...',
systemReset2:'数据信息正在重置', systemReset2:'数据信息正在重置',
}, },
ForcePasswdChange: {
title: '密码修改',
desc: '说明',
passwordPolicy: '密码策略强度',
passwordPolicyMsg: '至少{minLength}位,至少包含{specialChars}个特殊字符和至少{uppercase}大写字母和至少{lowercase}小写字母',
passwdExpire: '密码有效期',
passwdExpireMsg: '有效期为{expDay}天,请在过期前{alertDay}天进行密码修改',
},
}, },
// 静态路由 // 静态路由
@@ -163,7 +170,7 @@ export default {
userNameReg: '账号不能以数字开头可包含大写小写字母数字且不少于5位', userNameReg: '账号不能以数字开头可包含大写小写字母数字且不少于5位',
userNamePlease: '请输入正确登录账号', userNamePlease: '请输入正确登录账号',
userNameHit: '登录账号', userNameHit: '登录账号',
passwordReg: '密码至少包含大小写字母、数字、特殊符号且不少于6位', passwordReg: '请输入正确的密码格式',
passwordPlease: '请输入正确登录密码', passwordPlease: '请输入正确登录密码',
passwordHit: '登录密码', passwordHit: '登录密码',
passwordConfirmHit: '确认登录密码', passwordConfirmHit: '确认登录密码',
@@ -295,7 +302,7 @@ export default {
oldPasswordTip: "旧密码不能为空且不少于6位", oldPasswordTip: "旧密码不能为空且不少于6位",
oldPasswordPleace: "请输入旧密码", oldPasswordPleace: "请输入旧密码",
newPassword: "新密码", newPassword: "新密码",
newPasswordTip: "密码至少包含大小写字母、数字、特殊符号且不少于6位", newPasswordTip: "请输入正确的密码格式",
newPassworddPleace: "请输入新密码", newPassworddPleace: "请输入新密码",
confirmPassword: "确认新密码", confirmPassword: "确认新密码",
confirmPasswordPleace: "请确认新密码", confirmPasswordPleace: "请确认新密码",
@@ -506,7 +513,7 @@ export default {
delTip: '确认删除网元信息数据项吗?', delTip: '确认删除网元信息数据项吗?',
oam: { oam: {
title: 'OAM配置', title: 'OAM配置',
sync: '同步到网元', restart: '下发后重启网元',
oamEnable: '服务', oamEnable: '服务',
oamPort: '端口', oamPort: '端口',
snmpEnable: '服务', snmpEnable: '服务',
@@ -659,6 +666,19 @@ export default {
name: "名称", name: "名称",
downTip: '确认要下载备份文件【{txt}】吗?', downTip: '确认要下载备份文件【{txt}】吗?',
title: "修改备份信息 {txt}", title: "修改备份信息 {txt}",
backupModal: {
pushFileOper: "将当前文件发送到远程备份",
title: "设置远程备份服务",
enable: "启用",
toIp: "服务IP",
toIpPleace: "请输入远程备份服务器 IP 地址",
toPort: "服务端口",
username: "登录用户名",
usernamePleace: '请输入服务登录用户名',
password: "登录密码",
dir: "保存目录",
dirPleace: '请输入服务地址目标文件目录',
}
}, },
neQuickSetup: { neQuickSetup: {
reloadPara5G: '刷新', reloadPara5G: '刷新',
@@ -711,8 +731,45 @@ export default {
}, },
neData: { neData: {
common: { common: {
startIMSI: '起始IMSI',
imsi: 'IMSI',
imsiTip: 'IMSI=MCC+MNC+MSIN',
imsiTip1: 'MCC=移动国家号码, 由三位数字组成',
imsiTip2: 'MNC=移动网络号,由两位数字组成',
imsiTip3: 'MSIN=移动客户识别码采用等长10位数字构成',
imsiPlease: "请正确输入IMSI",
msisdn: '移动客户识别码',
msisdnPlease: "请正确输入移动客户识别码",
loadDataConfirm: '确认要重新加载数据吗?',
loadData: '加载数据',
loadDataTip: '成功获取加载数据:{num}条,系统内部正在进行数据更新,大约需要{timer}秒,请稍候!!!',
batchOper: '批量操作',
batchAddText: '批量新增',
batchDelText: '批量删除',
batchUpdateText: '批量更新',
batchNum: '批量个数',
checkDel:'勾选删除',
importTemplate: '导入模板', importTemplate: '导入模板',
}, },
udmVOIP: {
startUsername: '起始用户名',
username: '用户名',
usernamePlease: "请正确输入用户名",
password: "密码",
passwordPlease: "请正确输入密码",
addTitle: '新增VOIP用户',
delTip: '确认要删除VOIP用户为【{num}】的信息吗?',
exportTip: "确认根据搜索条件导出xlsx表格文件吗?",
},
udmVolteIMS: {
startMSISDN: '起始MSISDN',
voipTip: '当选择VoIP时MSISDN会等于IMSI',
addTitle: '新增IMS签约用户',
vniTip: '示例ims.mnc000.mcc000.3gppnetwork.org',
vniPlease: '请正确输入VNI',
delTip: '确认要删除IMS签约为【{num}】的信息吗?',
exportTip: "确认根据搜索条件导出xlsx表格文件吗?",
},
baseStation: { baseStation: {
list: "列表", list: "列表",
topology: "拓扑图", topology: "拓扑图",
@@ -735,6 +792,12 @@ export default {
exportTip: "确认根据搜索条件导出xlsx表格文件吗?", exportTip: "确认根据搜索条件导出xlsx表格文件吗?",
importDataEmpty: "导入数据为空", importDataEmpty: "导入数据为空",
}, },
backupData: {
auth: "UDM鉴权用户",
sub: "UDM签约用户",
voip: "VOIP鉴权用户",
volte: "IMS签约用户",
}
}, },
neUser: { neUser: {
auth: { auth: {
@@ -1004,25 +1067,6 @@ export default {
}, },
}, },
traceManage: { traceManage: {
analysis: {
imsi: 'IMSI',
imsiPlease: '请输入IMSI',
msisdn: 'MSISDN',
msisdnPlease: '请输入MSISDN',
trackTaskId: '跟踪任务标记',
srcIp: '源IP地址',
dstIp: '目标IP地址',
signalType: '信令类型',
msgDirect: '消息元',
msgType: '消息类型',
rowTime: '记录时间',
signalData: '信令数据',
signalDetail: '信令详情',
noData: '无信息内容',
taskTitle: '任务 {num}',
taskDownText: '下载HTML',
taskDownTip: '确认下载信令详情HTML文件?',
},
pcap: { pcap: {
capArgPlease: '请输入tcpdump -i any支持参数', capArgPlease: '请输入tcpdump -i any支持参数',
cmd: '命令', cmd: '命令',
@@ -1073,21 +1117,17 @@ export default {
imsiTip: '移动通信IMSI编号', imsiTip: '移动通信IMSI编号',
srcIp: '源IP地址', srcIp: '源IP地址',
srcIpPlease: '请输入源IP地址', srcIpPlease: '请输入源IP地址',
srcIpTip: '当前发送端IPv4地址', srcIpTip: '发送端IPv4地址',
dstIp: '目标IP地址', dstIp: '目标IP地址',
dstIpPlease: '请输入目标IP地址', dstIpPlease: '请输入目标IP地址',
dstIpTip: '对方接收端IPv4地址', dstIpTip: '接收端IPv4地址',
interfaces: '信令接口', interfaces: '信令接口',
interfacesPlease: '请输入信令接口', interfacesPlease: '请输入信令接口',
signalPort: '信令端口', rangePicker: '任务时间',
signalPortPlease: '请输入信令端口',
signalPortTip: '目标IP地址或源IP地址对应一方的端口',
rangePicker: '开始结束时间',
rangePickerPlease: '请选择任务时间开始结束时间', rangePickerPlease: '请选择任务时间开始结束时间',
remark: '说明', remark: '说明',
remarkPlease: '可输入任务说明', remarkPlease: '可输入任务说明',
addTask: '添加任务', addTask: '添加任务',
editTask: '修改任务',
viewTask: '查看任务', viewTask: '查看任务',
errorTaskInfo: '获取任务信息失败', errorTaskInfo: '获取任务信息失败',
delTaskTip: '确认删除记录编号为 {id} 的数据项?', delTaskTip: '确认删除记录编号为 {id} 的数据项?',
@@ -1097,6 +1137,15 @@ export default {
traceFile: "跟踪文件", traceFile: "跟踪文件",
errMsg: "错误信息", errMsg: "错误信息",
imsiORmsisdn: "imsi 或 msisdn 是空值,不能开始任务", imsiORmsisdn: "imsi 或 msisdn 是空值,不能开始任务",
dataView: "跟踪数据",
protocolOrInterface: "协议/接口",
msgNe: '消息网元',
msgEvent: '消息事件',
msgType: '消息类型',
msgDirect: '消息方向',
msgLen: '消息长度',
rowTime: '消息时间',
taskInfo: '任务信息',
}, },
}, },
faultManage: { faultManage: {
@@ -1141,13 +1190,11 @@ export default {
delSuss:'清除成功', delSuss:'清除成功',
delSure:'是否清除该告警', delSure:'是否清除该告警',
showSet:'显示过滤设置', showSet:'显示过滤设置',
exportTip: "确认根据搜索条件导出xlsx表格文件吗?",
exportSure:'确认是否导出全部活动告警信息', exportSure:'确认是否导出全部活动告警信息',
viewIdInfo:'查看{alarmId} 记录信息', viewIdInfo:'查看{alarmId} 记录信息',
closeModal:'关闭', closeModal:'关闭',
}, },
historyAlarm:{
exportSure:'确认是否导出全部历史告警信息?',
},
faultSetting:{ faultSetting:{
interfaceType:'类型', interfaceType:'类型',
email:'Email', email:'Email',
@@ -1179,7 +1226,7 @@ export default {
type:'网元类型', type:'网元类型',
neId:'网元唯一标识', neId:'网元唯一标识',
MML:'MML', MML:'MML',
logTime:'log Time' logTime:'记录时间'
}, },
forwarding:{ forwarding:{
alarmId:'告警唯一标识', alarmId:'告警唯一标识',
@@ -1216,12 +1263,17 @@ export default {
tailLines: '末尾行数', tailLines: '末尾行数',
}, },
exportFile:{ exportFile:{
fileName:'文件来源', fileSource:'文件来源',
fileSourcePlease:'请选择文件来源',
downTip: "确认下载文件名为 【{fileName}】 文件?", downTip: "确认下载文件名为 【{fileName}】 文件?",
downTipErr: "文件获取失败", downTipErr: "文件获取失败",
deleteTip: "确认删除文件名为 【{fileName}】 文件?", deleteTip: "确认删除文件名为 【{fileName}】 文件?",
deleteTipErr: "文件删除失败", deleteTipErr: "文件删除失败",
selectTip:"请选择文件名", operateLog:'操作日志',
cdrIMS:'语音话单',
cdrSMF:'数据话单',
cdrSMSC:'短信话单',
cdrSGWC:'漫游数据话单',
} }
}, },
monitor: { monitor: {
@@ -1599,7 +1651,7 @@ export default {
loginTime: '登录时间', loginTime: '登录时间',
status: '用户状态', status: '用户状态',
userNameTip:'账号只能包含大写字母、小写字母和数字的字符串长度至少为6位', userNameTip:'账号只能包含大写字母、小写字母和数字的字符串长度至少为6位',
passwdTip:'密码至少包含大小写字母、数字、特殊符号,且不少于6位', passwdTip:'请输入正确的密码格式',
nickNameTip:'昵称不少于2位', nickNameTip:'昵称不少于2位',
emailTip:'请输入正确的邮箱地址', emailTip:'请输入正确的邮箱地址',
phoneTip:'请输入正确的手机号码', phoneTip:'请输入正确的手机号码',

View File

@@ -8,6 +8,7 @@ import {
import RightContent from './components/RightContent.vue'; import RightContent from './components/RightContent.vue';
import Tabs from './components/Tabs.vue'; import Tabs from './components/Tabs.vue';
import GlobalMask from '@/components/GlobalMask/index.vue'; import GlobalMask from '@/components/GlobalMask/index.vue';
import ForcePasswdChange from '@/components/ForcePasswdChange/index.vue';
import { scriptUrl } from '@/assets/js/icon_font_8d5l8fzk5b87iudi'; import { scriptUrl } from '@/assets/js/icon_font_8d5l8fzk5b87iudi';
import { import {
computed, computed,
@@ -362,6 +363,8 @@ onUnmounted(() => {
<!-- 全局遮罩 --> <!-- 全局遮罩 -->
<GlobalMask /> <GlobalMask />
<!-- 强制密码修改 -->
<ForcePasswdChange />
</a-watermark> </a-watermark>
</template> </template>

View File

@@ -1,25 +1,53 @@
import Cookies from 'js-cookie'; import Cookies from 'js-cookie';
import { TOKEN_COOKIE } from '@/constants/token-constants';
import { localRemove, localSet } from '@/utils/cache-local-utils'; import { localRemove, localSet } from '@/utils/cache-local-utils';
import { import {
CACHE_LOCAL_LOCK_PASSWD, CACHE_LOCAL_LOCK_PASSWD,
CACHE_LOCAL_MASK, CACHE_LOCAL_MASK,
} from '@/constants/cache-keys-constants'; } from '@/constants/cache-keys-constants';
import {
TOKEN_ACCESS_COOKIE,
TOKEN_REFRESH_COOKIE,
} from '@/constants/token-constants';
/**获取cookis中Token字符串 */ /**获取访问令牌 */
export function getToken(): string { export function getAccessToken(): string {
return Cookies.get(TOKEN_COOKIE) || ''; return Cookies.get(TOKEN_ACCESS_COOKIE) || '';
} }
/**设置cookis中Token字符串 */ /**
export function setToken(token: string): void { * 设置访问令牌
Cookies.set(TOKEN_COOKIE, token || ''); * @param token token字符串
* @param exp 过期时间(秒)
*/
export function setAccessToken(token: string, exp: number): void {
const expires = new Date(new Date().getTime() + exp * 1000);
Cookies.set(TOKEN_ACCESS_COOKIE, token, { expires });
localSet(CACHE_LOCAL_MASK, 'none'); localSet(CACHE_LOCAL_MASK, 'none');
} }
/**移除cookis中Token字符串,localStorage中锁屏字符串 */ /**移除访问令牌 */
export function removeToken(): void { export function delAccessToken(): void {
Cookies.remove(TOKEN_COOKIE); Cookies.remove(TOKEN_ACCESS_COOKIE);
localRemove(CACHE_LOCAL_MASK); localRemove(CACHE_LOCAL_MASK);
localRemove(CACHE_LOCAL_LOCK_PASSWD); localRemove(CACHE_LOCAL_LOCK_PASSWD);
} }
/**获取刷新令牌 */
export function getRefreshToken(): string {
return Cookies.get(TOKEN_REFRESH_COOKIE) || '';
}
/**
* 设置刷新令牌
* @param token token字符串
* @param exp 过期时间(秒)
*/
export function setRefreshToken(token: string, exp: number): void {
const expires = new Date(new Date().getTime() + exp * 1000);
Cookies.set(TOKEN_REFRESH_COOKIE, token, { expires });
}
/**移除刷新令牌 */
export function delRefreshToken(): void {
Cookies.remove(TOKEN_REFRESH_COOKIE);
}

View File

@@ -1,10 +1,16 @@
import { getToken, removeToken } from '@/plugins/auth-token'; import {
getAccessToken,
setAccessToken,
delAccessToken,
getRefreshToken,
setRefreshToken,
delRefreshToken,
} from '@/plugins/auth-token';
import { import {
sessionGet, sessionGet,
sessionGetJSON, sessionGetJSON,
sessionSetJSON, sessionSetJSON,
} from '@/utils/cache-session-utils'; } from '@/utils/cache-session-utils';
import { localGet } from '@/utils/cache-local-utils';
import { TOKEN_KEY, TOKEN_KEY_PREFIX } from '@/constants/token-constants'; import { TOKEN_KEY, TOKEN_KEY_PREFIX } from '@/constants/token-constants';
import { import {
CACHE_LOCAL_I18N, CACHE_LOCAL_I18N,
@@ -18,6 +24,7 @@ import {
import { import {
RESULT_CODE_ENCRYPT, RESULT_CODE_ENCRYPT,
RESULT_CODE_ERROR, RESULT_CODE_ERROR,
RESULT_CODE_EXCEPTION,
RESULT_CODE_SUCCESS, RESULT_CODE_SUCCESS,
RESULT_MSG_ENCRYPT, RESULT_MSG_ENCRYPT,
RESULT_MSG_ERROR, RESULT_MSG_ERROR,
@@ -25,10 +32,11 @@ import {
RESULT_MSG_SERVER_ERROR, RESULT_MSG_SERVER_ERROR,
RESULT_MSG_SUCCESS, RESULT_MSG_SUCCESS,
RESULT_MSG_TIMEOUT, RESULT_MSG_TIMEOUT,
RESULT_MSG_URL_NOTFOUND,
RESULT_MSG_URL_RESUBMIT, RESULT_MSG_URL_RESUBMIT,
} from '@/constants/result-constants'; } from '@/constants/result-constants';
import { decryptAES, encryptAES } from '@/utils/encrypt-utils'; import { decryptAES, encryptAES } from '@/utils/encrypt-utils';
import { localGet } from '@/utils/cache-local-utils';
import { refreshToken } from '@/api/auth';
/**响应结果类型 */ /**响应结果类型 */
export type ResultType = { export type ResultType = {
@@ -61,7 +69,7 @@ type OptionsType = {
/**请求地址 */ /**请求地址 */
url: string; url: string;
/**请求方法 */ /**请求方法 */
method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH'; method: 'GET' | 'POST' | 'PUT' | 'DELETE';
/**请求头 */ /**请求头 */
headers?: HeadersInit; headers?: HeadersInit;
/**地址栏参数 */ /**地址栏参数 */
@@ -133,16 +141,21 @@ function beforeRequest(options: OptionsType): OptionsType | Promise<any> {
Reflect.set(options.headers, 'Accept-Language', `${language};q=0.9`); Reflect.set(options.headers, 'Accept-Language', `${language};q=0.9`);
// 是否需要设置 token // 是否需要设置 token
const token = getToken(); const accessToken = getAccessToken();
if (options.whithToken && token) { if (options.whithToken && accessToken) {
Reflect.set(options.headers, TOKEN_KEY, TOKEN_KEY_PREFIX + token); Reflect.set(
options.headers,
TOKEN_KEY,
TOKEN_KEY_PREFIX + ' ' + accessToken
);
} }
// 是否需要防止数据重复提交 // 是否需要防止数据重复提交
if ( if (
options.repeatSubmit && options.repeatSubmit &&
options.dataType === 'json' && options.dataType === 'json' &&
['post', 'put'].includes(options.method) !(options.data instanceof FormData) &&
['POST', 'PUT'].includes(options.method)
) { ) {
const requestObj: RepeatSubmitType = { const requestObj: RepeatSubmitType = {
url: options.url, url: options.url,
@@ -212,13 +225,31 @@ function beforeRequest(options: OptionsType): OptionsType | Promise<any> {
return options; return options;
} }
/**请求后的拦截 */ /**响应前的拦截 */
function interceptorResponse(res: ResultType): ResultType | Promise<any> { async function beforeResponse(
options: OptionsType,
res: ResultType
): Promise<any> {
// console.log('请求后的拦截', res); // console.log('请求后的拦截', res);
// 登录失效时,移除授权令牌并重新刷新页面 // 登录失效时,移除授权令牌并重新刷新页面
if (res.code === 401) { // 登录失效时,移除访问令牌并重新请求
removeToken(); if (res.code === 401001) {
const result = await refreshToken(getRefreshToken());
// 更新访问令牌和刷新令牌
if (result.code === RESULT_CODE_SUCCESS) {
setAccessToken(result.data.accessToken, result.data.refreshExpiresIn);
setRefreshToken(result.data.refreshToken, result.data.refreshExpiresIn);
return await request(options);
} else {
delAccessToken();
delRefreshToken();
window.location.reload();
}
}
if ([401002, 401003].includes(res.code)) {
delAccessToken();
delRefreshToken();
window.location.reload(); window.location.reload();
} }
@@ -271,43 +302,45 @@ function interceptorResponse(res: ResultType): ResultType | Promise<any> {
* @returns 返回 Promise<ResultType> * @returns 返回 Promise<ResultType>
*/ */
export async function request(options: OptionsType): Promise<ResultType> { export async function request(options: OptionsType): Promise<ResultType> {
options = Object.assign({}, FATCH_OPTIONS, options); let reqOptions = Object.assign({}, FATCH_OPTIONS, options);
let timeoutId: any = 0;
// 请求超时控制请求终止 // 请求超时控制请求终止
if (!options.signal) { let timeoutId: any = null;
if (!reqOptions.signal) {
const controller = new AbortController(); const controller = new AbortController();
const { signal } = controller; reqOptions.signal = controller.signal;
options.signal = signal;
timeoutId = setTimeout(() => { timeoutId = setTimeout(() => {
controller.abort(); // 终止请求 controller.abort(); // 终止请求
}, options.timeout); }, reqOptions.timeout);
} }
// 检查请求拦截 // 检查请求拦截
const beforeReq = beforeRequest(options); const beforeReq = beforeRequest(reqOptions);
if (beforeReq instanceof Promise) { if (beforeReq instanceof Promise) {
return await beforeReq; return await beforeReq;
} }
options = beforeReq; reqOptions = beforeReq;
// 判断用户传递的URL是否http或/开头 // 判断用户传递的URL是否http或/开头
if (!options.url.startsWith('http')) { if (!reqOptions.url.startsWith('http')) {
const uri = options.url.startsWith('/') ? options.url : `/${options.url}`; const uri = reqOptions.url.startsWith('/')
options.url = options.baseUrl + uri; ? reqOptions.url
: `/${reqOptions.url}`;
reqOptions.url = reqOptions.baseUrl + uri;
} }
try { try {
const res = await fetch(options.url, options); const res = await fetch(reqOptions.url, reqOptions);
// console.log('请求结果:', res); // console.log('请求结果:', res);
if (res.status === 500) {
// 状态码拦截处理 return {
const reqNot = stateCode(res); code: RESULT_CODE_EXCEPTION,
if (reqNot != false) { msg: RESULT_MSG_SERVER_ERROR[language],
return reqNot; };
} }
// 根据响应数据类型返回 // 根据响应数据类型返回
switch (options.responseType) { switch (reqOptions.responseType) {
case 'text': // 文本数据 case 'text': // 文本数据
const str = await res.text(); const str = await res.text();
return { return {
@@ -317,11 +350,7 @@ export async function request(options: OptionsType): Promise<ResultType> {
case 'json': // json格式数据 case 'json': // json格式数据
const result = await res.json(); const result = await res.json();
// 请求后的拦截 // 请求后的拦截
const beforeRes = interceptorResponse(result); return await beforeResponse(options, result);
if (beforeRes instanceof Promise) {
return await beforeRes;
}
return result;
case 'blob': // 二进制数据则直接返回 case 'blob': // 二进制数据则直接返回
case 'arrayBuffer': case 'arrayBuffer':
const contentType = res.headers.get('content-type') || ''; const contentType = res.headers.get('content-type') || '';
@@ -330,7 +359,7 @@ export async function request(options: OptionsType): Promise<ResultType> {
return result as ResultType; return result as ResultType;
} }
const data = const data =
options.responseType === 'blob' reqOptions.responseType === 'blob'
? await res.blob() ? await res.blob()
: await res.arrayBuffer(); : await res.arrayBuffer();
return { return {
@@ -356,41 +385,7 @@ export async function request(options: OptionsType): Promise<ResultType> {
} }
throw error; throw error;
} finally { } finally {
clearTimeout(timeoutId); // 请求成功,清除超时计时器 clearTimeout(timeoutId); // 清除超时计时器
timeoutId = null;
} }
} }
/**
* 判断状态码处理结果信息(不可处理)
* @param res 请求结果
* @returns
*/
function stateCode(res: Response) {
// 网络异常
if (res.status === 500) {
return {
code: RESULT_CODE_ERROR,
msg: RESULT_MSG_SERVER_ERROR[language],
};
}
// 上传文件成功无内容返回
if (res.status === 204 || res.statusText === 'No Content') {
return {
code: RESULT_CODE_SUCCESS,
msg: RESULT_MSG_SUCCESS[language],
};
}
// 地址找不到
if (res.status === 404 || res.status === 405) {
return {
code: RESULT_CODE_ERROR,
msg: RESULT_MSG_URL_NOTFOUND[language],
};
}
// 身份授权
if (res.status === 401) {
removeToken();
window.location.reload();
}
return false;
}

View File

@@ -1,5 +1,5 @@
import { sessionGet } from '@/utils/cache-session-utils'; import { sessionGet } from '@/utils/cache-session-utils';
import { getToken } from './auth-token'; import { getAccessToken } from './auth-token';
import { localGet } from '@/utils/cache-local-utils'; import { localGet } from '@/utils/cache-local-utils';
import { CACHE_LOCAL_I18N } from '@/constants/cache-keys-constants'; import { CACHE_LOCAL_I18N } from '@/constants/cache-keys-constants';
import { TOKEN_RESPONSE_FIELD } from '@/constants/token-constants'; import { TOKEN_RESPONSE_FIELD } from '@/constants/token-constants';
@@ -92,7 +92,7 @@ export class WS {
// 地址栏参数 // 地址栏参数
let params = Object.assign({}, options.params, { let params = Object.assign({}, options.params, {
// 设置 token // 设置 token
[TOKEN_RESPONSE_FIELD]: getToken(), [TOKEN_RESPONSE_FIELD]: getAccessToken(),
// 多语言 // 多语言
['language']: localGet(CACHE_LOCAL_I18N) || 'en_US', ['language']: localGet(CACHE_LOCAL_I18N) || 'en_US',
}); });
@@ -119,17 +119,15 @@ export class WS {
}; };
// 用于指定当从服务器接受到信息时的回调函数。 // 用于指定当从服务器接受到信息时的回调函数。
ws.onmessage = ev => { ws.onmessage = ev => {
if (ev.type !== 'message') return;
// 解析文本消息 // 解析文本消息
if (ev.type === 'message') { try {
const data = ev.data; const jsonData = JSON.parse(ev.data);
try { if (typeof options.onmessage === 'function') {
const jsonData = JSON.parse(data); options.onmessage(jsonData);
if (typeof options.onmessage === 'function') {
options.onmessage(jsonData);
}
} catch (error) {
console.error('websocket message formatting error', error);
} }
} catch (error) {
console.error('websocket message formatting error', error);
} }
}; };
// 用于指定连接关闭后的回调函数。 // 用于指定连接关闭后的回调函数。
@@ -221,7 +219,7 @@ export class WS {
this.heartInterval = window.setInterval(() => { this.heartInterval = window.setInterval(() => {
this.send({ this.send({
requestId: `${Date.now()}`, requestId: `${Date.now()}`,
type: 'ping', type: 'PING',
}); });
}, heartTimer); }, heartTimer);
} }

View File

@@ -8,7 +8,7 @@ import NProgress from 'nprogress';
import 'nprogress/nprogress.css'; import 'nprogress/nprogress.css';
import BasicLayout from '../layouts/BasicLayout.vue'; import BasicLayout from '../layouts/BasicLayout.vue';
import BlankLayout from '../layouts/BlankLayout.vue'; import BlankLayout from '../layouts/BlankLayout.vue';
import { getToken } from '@/plugins/auth-token'; import { getAccessToken } from '@/plugins/auth-token';
import { validHttp } from '@/utils/regular-utils'; import { validHttp } from '@/utils/regular-utils';
import useUserStore from '@/store/modules/user'; import useUserStore from '@/store/modules/user';
import useAppStore from '@/store/modules/app'; import useAppStore from '@/store/modules/app';
@@ -182,7 +182,7 @@ router.beforeEach(async (to, from, next) => {
// next({ name: 'Index' }); // next({ name: 'Index' });
// } // }
let token = getToken(); let token = getAccessToken();
// 免用户登录认证 // 免用户登录认证
if (!appStore.loginAuth) { if (!appStore.loginAuth) {

View File

@@ -1,7 +1,9 @@
import { getSysConf } from '@/api'; import { getSysConf } from '@/api';
import { CACHE_LOCAL_I18N, CACHE_SESSION_CRYPTO_API } from '@/constants/cache-keys-constants'; import {
CACHE_LOCAL_I18N,
CACHE_SESSION_CRYPTO_API,
} from '@/constants/cache-keys-constants';
import { RESULT_CODE_SUCCESS } from '@/constants/result-constants'; import { RESULT_CODE_SUCCESS } from '@/constants/result-constants';
// import { removeToken } from '@/plugins/auth-token';
import { parseUrlPath } from '@/plugins/file-static-url'; import { parseUrlPath } from '@/plugins/file-static-url';
import { localGet, localSet } from '@/utils/cache-local-utils'; import { localGet, localSet } from '@/utils/cache-local-utils';
import { sessionSet } from '@/utils/cache-session-utils'; import { sessionSet } from '@/utils/cache-session-utils';
@@ -16,11 +18,12 @@ type AppStore = {
/**应用版本 */ /**应用版本 */
appVersion: string; appVersion: string;
/**服务版本 */ /**版本 */
version: string; version: string;
// buildTime: string;
/**系统引导使用 */ /**系统引导使用 */
// bootloader: boolean; // bootloader: boolean;
/**服务版本 */
serverVersion: string;
// 用户登录认证 // 用户登录认证
loginAuth: boolean; loginAuth: boolean;
// 用户接口加密 // 用户接口加密
@@ -54,13 +57,13 @@ const useAppStore = defineStore('app', {
appCode: import.meta.env.VITE_APP_CODE, appCode: import.meta.env.VITE_APP_CODE,
appVersion: import.meta.env.VITE_APP_VERSION, appVersion: import.meta.env.VITE_APP_VERSION,
version: `-`, version: '-',
// buildTime: `-`,
// bootloader: false, // bootloader: false,
serverVersion: '-',
loginAuth: true, loginAuth: true,
cryptoApi: true, cryptoApi: true,
serialNum: `-`, serialNum: '-',
copyright: `Copyright ©2023 For ${import.meta.env.VITE_APP_NAME}`, copyright: `Copyright ©2023-2025 For ${import.meta.env.VITE_APP_NAME}`,
logoType: 'icon', logoType: 'icon',
filePathIcon: '', filePathIcon: '',
filePathBrand: '', filePathBrand: '',
@@ -86,15 +89,16 @@ const useAppStore = defineStore('app', {
const res = await getSysConf(); const res = await getSysConf();
if (res.code === RESULT_CODE_SUCCESS && res.data) { if (res.code === RESULT_CODE_SUCCESS && res.data) {
this.version = res.data.version; this.version = res.data.version;
// this.buildTime = res.data.buildTime; this.serverVersion = res.data.serverVersion;
// this.bootloader = res.data.bootloader === 'true'; // this.bootloader = res.data.bootloader === 'true';
// // 引导时 // // 引导时
// if (this.bootloader) { // if (this.bootloader) {
// removeToken(); // delAccessToken();
// delRefreshToken();
// } // }
this.loginAuth = res.data.loginAuth !== 'false'; this.loginAuth = res.data.loginAuth !== 'false';
this.cryptoApi = res.data.cryptoApi !== 'false'; this.cryptoApi = res.data.cryptoApi !== 'false';
sessionSet(CACHE_SESSION_CRYPTO_API, res.data.cryptoApi); sessionSet(CACHE_SESSION_CRYPTO_API, res.data.cryptoApi);
this.serialNum = res.data.serialNum; this.serialNum = res.data.serialNum;
this.appName = res.data.title; this.appName = res.data.title;
this.copyright = res.data.copyright; this.copyright = res.data.copyright;

View File

@@ -20,7 +20,7 @@ type MaskStateType = {
const useMaskStore = defineStore('mask', { const useMaskStore = defineStore('mask', {
state: (): MaskStateType => ({ state: (): MaskStateType => ({
type: (localGet(CACHE_LOCAL_MASK) || 'none') as MaskStateType['type'], type: (localGet(CACHE_LOCAL_MASK) || 'none') as MaskStateType['type'],
lockPasswd: localGet(CACHE_LOCAL_LOCK_PASSWD) || '', lockPasswd: atob(localGet(CACHE_LOCAL_LOCK_PASSWD) || ''),
lockTimeout: 0, lockTimeout: 0,
}), }),
getters: {}, getters: {},
@@ -59,7 +59,7 @@ const useMaskStore = defineStore('mask', {
}, 5_000); }, 5_000);
} }
if (type === 'lock') { if (type === 'lock') {
localSet(CACHE_LOCAL_LOCK_PASSWD, this.lockPasswd); localSet(CACHE_LOCAL_LOCK_PASSWD, btoa(this.lockPasswd));
} else { } else {
localRemove(CACHE_LOCAL_LOCK_PASSWD); localRemove(CACHE_LOCAL_LOCK_PASSWD);
} }

View File

@@ -2,7 +2,6 @@ import { defineStore } from 'pinia';
import { RESULT_CODE_SUCCESS } from '@/constants/result-constants'; import { RESULT_CODE_SUCCESS } from '@/constants/result-constants';
import { listAllNeInfo } from '@/api/ne/neInfo'; import { listAllNeInfo } from '@/api/ne/neInfo';
import { parseDataToOptions } from '@/utils/parse-tree-utils'; import { parseDataToOptions } from '@/utils/parse-tree-utils';
import { getNeTraceInterfaceAll } from '@/api/trace/task';
import { getNePerformanceList } from '@/api/perfManage/taskManage'; import { getNePerformanceList } from '@/api/perfManage/taskManage';
/**网元信息类型 */ /**网元信息类型 */
@@ -13,8 +12,6 @@ type NeInfo = {
neCascaderOptions: Record<string, any>[]; neCascaderOptions: Record<string, any>[];
/**选择器单级父类型 */ /**选择器单级父类型 */
neSelectOtions: Record<string, any>[]; neSelectOtions: Record<string, any>[];
/**跟踪接口列表 */
traceInterfaceList: Record<string, any>[];
/**性能测量数据集 */ /**性能测量数据集 */
perMeasurementList: Record<string, any>[]; perMeasurementList: Record<string, any>[];
}; };
@@ -24,7 +21,6 @@ const useNeInfoStore = defineStore('neinfo', {
neList: [], neList: [],
neCascaderOptions: [], neCascaderOptions: [],
neSelectOtions: [], neSelectOtions: [],
traceInterfaceList: [],
perMeasurementList: [], perMeasurementList: [],
}), }),
getters: { getters: {
@@ -56,7 +52,7 @@ const useNeInfoStore = defineStore('neinfo', {
async fnNelist() { async fnNelist() {
// 有数据不请求 // 有数据不请求
if (this.neList.length > 0) { if (this.neList.length > 0) {
return { code: 1, data: this.neList, msg: 'success' }; return { code: RESULT_CODE_SUCCESS, data: this.neList, msg: 'success' };
} }
const res = await listAllNeInfo({ const res = await listAllNeInfo({
bandStatus: false, bandStatus: false,
@@ -79,29 +75,11 @@ const useNeInfoStore = defineStore('neinfo', {
} }
return res; return res;
}, },
// 刷新跟踪接口列表
async fnRefreshNeTraceInterface() {
this.traceInterfaceList = [];
const res = await this.fnNeTraceInterface();
return res;
},
// 获取跟踪接口列表
async fnNeTraceInterface() {
// 有数据不请求
if (this.traceInterfaceList.length > 0) {
return { code: 1, data: this.traceInterfaceList, msg: 'success' };
}
const res = await getNeTraceInterfaceAll();
if (res.code === RESULT_CODE_SUCCESS) {
this.traceInterfaceList = res.data;
}
return res;
},
// 获取性能测量数据集列表 // 获取性能测量数据集列表
async fnNeTaskPerformance() { async fnNeTaskPerformance() {
// 有数据不请求 // 有数据不请求
if (this.perMeasurementList.length > 0) { if (this.perMeasurementList.length > 0) {
return { code: 1, data: this.perMeasurementList, msg: 'success' }; return { code: RESULT_CODE_SUCCESS, data: this.perMeasurementList, msg: 'success' };
} }
const res = await getNePerformanceList(); const res = await getNePerformanceList();
if (res.code === RESULT_CODE_SUCCESS) { if (res.code === RESULT_CODE_SUCCESS) {

View File

@@ -5,7 +5,7 @@ import type {
RouteMeta, RouteMeta,
RouteRecordRaw, RouteRecordRaw,
} from 'vue-router'; } from 'vue-router';
import { getRouters } from '@/api/router'; import { getRouter } from '@/api/auth';
import BasicLayout from '@/layouts/BasicLayout.vue'; import BasicLayout from '@/layouts/BasicLayout.vue';
import BlankLayout from '@/layouts/BlankLayout.vue'; import BlankLayout from '@/layouts/BlankLayout.vue';
import LinkLayout from '@/layouts/LinkLayout.vue'; import LinkLayout from '@/layouts/LinkLayout.vue';
@@ -46,7 +46,7 @@ const useRouterStore = defineStore('router', {
* @returns 生成的路由菜单 * @returns 生成的路由菜单
*/ */
async generateRoutes() { async generateRoutes() {
const res = await getRouters(); const res = await getRouter();
if (res.code === RESULT_CODE_SUCCESS) { if (res.code === RESULT_CODE_SUCCESS) {
const buildRoutes = buildRouters(res.data.concat()); const buildRoutes = buildRouters(res.data.concat());
this.buildRouterData = buildRoutes; this.buildRouterData = buildRoutes;

View File

@@ -1,14 +1,20 @@
import defaultAvatar from '@/assets/images/default_avatar.png'; import defaultAvatar from '@/assets/images/default_avatar.png';
import useLayoutStore from './layout'; import useLayoutStore from './layout';
import { login, logout, getInfo } from '@/api/login'; import { login, logout, getInfo } from '@/api/auth';
import { setToken, removeToken } from '@/plugins/auth-token'; import {
delAccessToken,
delRefreshToken,
setAccessToken,
setRefreshToken,
} from '@/plugins/auth-token';
import { defineStore } from 'pinia'; import { defineStore } from 'pinia';
import { TOKEN_RESPONSE_FIELD } from '@/constants/token-constants'; import { RESULT_CODE_EXCEPTION, RESULT_CODE_SUCCESS } from '@/constants/result-constants';
import { RESULT_CODE_SUCCESS } from '@/constants/result-constants';
import { parseUrlPath } from '@/plugins/file-static-url'; import { parseUrlPath } from '@/plugins/file-static-url';
/**用户信息类型 */ /**用户信息类型 */
type UserInfo = { type UserInfo = {
/**用户ID */
forcePasswdChange: boolean;
/**用户ID */ /**用户ID */
userId: string; userId: string;
/**登录账号 */ /**登录账号 */
@@ -33,6 +39,7 @@ type UserInfo = {
const useUserStore = defineStore('user', { const useUserStore = defineStore('user', {
state: (): UserInfo => ({ state: (): UserInfo => ({
forcePasswdChange: false,
userId: '', userId: '',
userName: '', userName: '',
roles: [], roles: [],
@@ -102,8 +109,11 @@ const useUserStore = defineStore('user', {
async fnLogin(loginBody: Record<string, string>) { async fnLogin(loginBody: Record<string, string>) {
const res = await login(loginBody); const res = await login(loginBody);
if (res.code === RESULT_CODE_SUCCESS && res.data) { if (res.code === RESULT_CODE_SUCCESS && res.data) {
const token = res.data[TOKEN_RESPONSE_FIELD]; setAccessToken(res.data.accessToken, res.data.refreshExpiresIn);
setToken(token); setRefreshToken(res.data.refreshToken, res.data.refreshExpiresIn);
if (res.data?.forcePasswdChange) {
this.forcePasswdChange = true;
}
} }
return res; return res;
}, },
@@ -139,10 +149,15 @@ const useUserStore = defineStore('user', {
// } // }
// useLayoutStore().changeWaterMark(waterMarkContent); // useLayoutStore().changeWaterMark(waterMarkContent);
useLayoutStore().changeWaterMark(''); useLayoutStore().changeWaterMark('');
// 强制修改密码
if (res.data?.forcePasswdChange) {
this.forcePasswdChange = true;
}
} }
// 网络错误时退出登录状态 // 网络错误时退出登录状态
if (res.code === 0) { if (res.code === RESULT_CODE_EXCEPTION) {
removeToken(); delAccessToken();
delRefreshToken();
window.location.reload(); window.location.reload();
} }
return res; return res;
@@ -156,7 +171,8 @@ const useUserStore = defineStore('user', {
} finally { } finally {
this.roles = []; this.roles = [];
this.permissions = []; this.permissions = [];
removeToken(); delAccessToken();
delRefreshToken();
} }
}, },
}, },

View File

@@ -20,8 +20,10 @@ export const YYYYMMDDHHMMSS = 'YYYYMMDDHHmmss';
/**年-月-日 时:分:秒 列如2022-12-30 01:01:59 */ /**年-月-日 时:分:秒 列如2022-12-30 01:01:59 */
export const YYYY_MM_DD_HH_MM_SS = 'YYYY-MM-DD HH:mm:ss'; export const YYYY_MM_DD_HH_MM_SS = 'YYYY-MM-DD HH:mm:ss';
/**特殊 列如2025-04-28 08:00:46 GMT+08:00 */ /**国际时间 列如2022-12-30T01:01:59+08:00 */
export const YYYY_MM_DD_HH_MM_SSZ = 'YYYY-MM-DD HH:mm:ss [GMT]Z'; export const RFC3339 = 'YYYY-MM-DDTHH:mm:ssZ';
/**国际时间 列如Thu, Nov 14 2024 10:19 GMT+08:00 */
export const RFC822Z = 'ddd, MMM DD YYYY HH:mm [GMT]Z';
/** /**
* 格式时间字符串 * 格式时间字符串
@@ -39,12 +41,12 @@ export function parseStrToDate(
/** /**
* 格式时间 * 格式时间
* @param date 可转的Date对象 * @param date 可转的Date对象
* @param formatStr 时间格式 默认YYYY-MM-DD HH:mm:ssZZ * @param formatStr 时间格式 默认 RFC3339
* @returns 时间格式字符串 * @returns 时间格式字符串
*/ */
export function parseDateToStr( export function parseDateToStr(
date: string | number | Date, date: string | number | Date,
formatStr: string = YYYY_MM_DD_HH_MM_SSZ formatStr: string = RFC3339
): string { ): string {
return dayjs(date).format(formatStr); return dayjs(date).format(formatStr);
} }

View File

@@ -101,7 +101,14 @@ export function parseObjLineToHump(obj: any): any {
* @param decimalPlaces 保留小数位,默认2位 * @param decimalPlaces 保留小数位,默认2位
* @returns 单位 xB * @returns 单位 xB
*/ */
export function parseSizeFromFile(bytes: number, decimalPlaces: number = 2) { export function parseSizeFromFile(
bytes: number,
decimalPlaces: number = 2
): string {
if (typeof bytes !== 'number' || isNaN(bytes) || bytes < 0) {
return `${bytes}`;
}
if (bytes === 0) return '0 B';
const units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; const units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
let i = 0; let i = 0;
while (bytes >= 1024 && i < units.length - 1) { while (bytes >= 1024 && i < units.length - 1) {

View File

@@ -27,14 +27,15 @@ export const regExpUserName = /^[A-Za-z0-9]{6,}$/;
* 有效密码格式 * 有效密码格式
* *
* 密码至少包含大小写字母、数字、特殊符号且不少于6位 * 密码至少包含大小写字母、数字、特殊符号且不少于6位
* /^(?![A-Za-z0-9]+$)(?![a-z0-9\W]+$)(?![A-Za-z\W]+$)(?![A-Z0-9\W]+$)[a-zA-Z0-9\W]{6,}$/
*/ */
export const regExpPasswd = /^.{5,}$/; export const regExpPasswd = /^.{6,}$/;
// /^(?![A-Za-z0-9]+$)(?![a-z0-9\W]+$)(?![A-Za-z\W]+$)(?![A-Z0-9\W]+$)[a-zA-Z0-9\W]{6,}$/;
/** /**
* 有效手机号格式 * 有效手机号格式
* /^1[3|4|5|6|7|8|9][0-9]\d{8}$/
*/ */
export const regExpMobile = /^.{3,}$/; // /^1[3|4|5|6|7|8|9][0-9]\d{8}$/; export const regExpMobile = /^.{3,}$/;
/** /**
* 有效邮箱格式 * 有效邮箱格式
@@ -46,8 +47,9 @@ export const regExpEmail =
* 有效用户昵称格式 * 有效用户昵称格式
* *
* 用户昵称只能包含字母、数字、中文和下划线且不少于2位 * 用户昵称只能包含字母、数字、中文和下划线且不少于2位
* /^[\w\u4e00-\u9fa5-]{2,}$/
*/ */
export const regExpNick = /^.{2,}$/; // /^[\w\u4e00-\u9fa5-]{2,}$/; export const regExpNick = /^.{2,}$/;
/** /**
* 是否为http(s)://开头 * 是否为http(s)://开头

View File

@@ -52,6 +52,7 @@ function fnFinish() {
updateUserPassword(state.form.oldPassword, state.form.confirmPassword) updateUserPassword(state.form.oldPassword, state.form.confirmPassword)
.then(res => { .then(res => {
if (res.code === RESULT_CODE_SUCCESS) { if (res.code === RESULT_CODE_SUCCESS) {
fnLogOut();
Modal.success({ Modal.success({
title: t('common.tipTitle'), title: t('common.tipTitle'),
content: t('views.account.settings.submitOkTip', { content: t('views.account.settings.submitOkTip', {
@@ -59,7 +60,7 @@ function fnFinish() {
}), }),
okText: t('views.account.settings.submitOk'), okText: t('views.account.settings.submitOk'),
onOk() { onOk() {
fnLogOut().finally(() => router.push({ name: 'Login' })); router.push({ name: 'Login' });
}, },
}); });
} else { } else {

View File

@@ -23,7 +23,9 @@ export const upfFlowData = ref<FDType>({
/**UPF-流量数据 数据解析 */ /**UPF-流量数据 数据解析 */
export function upfFlowParse(data: Record<string, string>) { export function upfFlowParse(data: Record<string, string>) {
upfFlowData.value.lineXTime.push(parseDateToStr(+data['timeGroup'], 'HH:mm:ss')); upfFlowData.value.lineXTime.push(
parseDateToStr(+data['timeGroup'], 'HH:mm:ss')
);
const upN3 = parseSizeFromKbs(+data['UPF.03'], 5); const upN3 = parseSizeFromKbs(+data['UPF.03'], 5);
upfFlowData.value.lineYUp.push(upN3[0]); upfFlowData.value.lineYUp.push(upN3[0]);
const downN6 = parseSizeFromKbs(+data['UPF.06'], 5); const downN6 = parseSizeFromKbs(+data['UPF.06'], 5);

View File

@@ -385,7 +385,7 @@ onBeforeUnmount(() => {
class="item toRouter" class="item toRouter"
:title="t('views.dashboard.overview.toRouter')" :title="t('views.dashboard.overview.toRouter')"
> >
<div @click="fnToRouter('Sub_2010')"> <div @click="fnToRouter('UdmSub_2001')">
<UserOutlined <UserOutlined
style="color: #4096ff; margin-right: 8px; font-size: 1.1rem" style="color: #4096ff; margin-right: 8px; font-size: 1.1rem"
/> />
@@ -399,11 +399,7 @@ onBeforeUnmount(() => {
</div> </div>
<template #overlay> <template #overlay>
<a-menu @click="fnSelectUDM"> <a-menu @click="fnSelectUDM">
<a-menu-item <a-menu-item v-for="v in udmOtions" :key="v.value" :disabled="udmNeId === v.value">
v-for="v in udmOtions"
:key="v.value"
:disabled="udmNeId === v.value"
>
{{ v.label }} {{ v.label }}
</a-menu-item> </a-menu-item>
</a-menu> </a-menu>
@@ -413,7 +409,7 @@ onBeforeUnmount(() => {
</div> </div>
<div <div
class="item toRouter" class="item toRouter"
@click="fnToRouter('Ims_2080')" @click="fnToRouter('ImsSub_2004')"
:title="t('views.dashboard.overview.toRouter')" :title="t('views.dashboard.overview.toRouter')"
style="margin: 0 12px" style="margin: 0 12px"
v-perms:has="['dashboard:overview:imsUeNum']" v-perms:has="['dashboard:overview:imsUeNum']"
@@ -428,7 +424,7 @@ onBeforeUnmount(() => {
</div> </div>
<div <div
class="item toRouter" class="item toRouter"
@click="fnToRouter('Ue_2081')" @click="fnToRouter('SmfSub_2005')"
:title="t('views.dashboard.overview.toRouter')" :title="t('views.dashboard.overview.toRouter')"
v-perms:has="['dashboard:overview:smfUeNum']" v-perms:has="['dashboard:overview:smfUeNum']"
> >
@@ -452,7 +448,7 @@ onBeforeUnmount(() => {
<div class="data"> <div class="data">
<div <div
class="item toRouter" class="item toRouter"
@click="fnToRouter('BaseStation_2096', { neType: 'AMF' })" @click="fnToRouter('BaseStation_2007', { neType: 'AMF' })"
:title="t('views.dashboard.overview.toRouter')" :title="t('views.dashboard.overview.toRouter')"
> >
<div style="align-items: flex-start"> <div style="align-items: flex-start">
@@ -466,7 +462,7 @@ onBeforeUnmount(() => {
</div> </div>
<div <div
class="item toRouter" class="item toRouter"
@click="fnToRouter('BaseStation_2096', { neType: 'AMF' })" @click="fnToRouter('BaseStation_2007', { neType: 'AMF' })"
:title="t('views.dashboard.overview.toRouter')" :title="t('views.dashboard.overview.toRouter')"
> >
<div style="align-items: flex-start"> <div style="align-items: flex-start">
@@ -489,7 +485,7 @@ onBeforeUnmount(() => {
<div class="data"> <div class="data">
<div <div
class="item toRouter" class="item toRouter"
@click="fnToRouter('BaseStation_2096', { neType: 'MME' })" @click="fnToRouter('BaseStation_2007', { neType: 'MME' })"
:title="t('views.dashboard.overview.toRouter')" :title="t('views.dashboard.overview.toRouter')"
> >
<div style="align-items: flex-start"> <div style="align-items: flex-start">
@@ -503,7 +499,7 @@ onBeforeUnmount(() => {
</div> </div>
<div <div
class="item toRouter" class="item toRouter"
@click="fnToRouter('BaseStation_2096', { neType: 'MME' })" @click="fnToRouter('BaseStation_2007', { neType: 'MME' })"
:title="t('views.dashboard.overview.toRouter')" :title="t('views.dashboard.overview.toRouter')"
> >
<div style="align-items: flex-start"> <div style="align-items: flex-start">

View File

@@ -14,6 +14,7 @@ import {
showPass, showPass,
getPass, getPass,
exportAll, exportAll,
exportAlarm,
} from '@/api/faultManage/actAlarm'; } from '@/api/faultManage/actAlarm';
import useI18n from '@/hooks/useI18n'; import useI18n from '@/hooks/useI18n';
import useNeInfoStore from '@/store/modules/neinfo'; import useNeInfoStore from '@/store/modules/neinfo';
@@ -730,6 +731,38 @@ function fnModalCancel() {
modalState.helpShowView = false; modalState.helpShowView = false;
} }
/**列表导出 */
function fnExportList() {
if (modalState.confirmLoading) return;
Modal.confirm({
title: t('common.tipTitle'),
content: t('views.faultManage.activeAlarm.exportTip'),
onOk() {
const hide = message.loading(t('common.loading'), 0);
const querys = toRaw(queryParams);
exportAlarm(querys)
.then(res => {
if (res.code === RESULT_CODE_SUCCESS) {
message.success({
content: t('common.operateOk'),
duration: 3,
});
saveAs(res.data, `active_alarm_export_${Date.now()}.xlsx`);
} else {
message.error({
content: `${res.msg}`,
duration: 3,
});
}
})
.finally(() => {
hide();
modalState.confirmLoading = false;
});
},
});
}
/**查询列表, pageNum初始页数 */ /**查询列表, pageNum初始页数 */
function fnGetList(pageNum?: number) { function fnGetList(pageNum?: number) {
if (tableState.loading) return; if (tableState.loading) return;
@@ -934,6 +967,11 @@ onMounted(() => {
{{ t('views.faultManage.activeAlarm.syncMyself') }} {{ t('views.faultManage.activeAlarm.syncMyself') }}
</a-button> </a-button>
<a-button type="primary" @click.prevent="fnShowSet()" v-if="false">
<template #icon> <SettingOutlined /> </template>
{{ t('views.faultManage.activeAlarm.disPlayFilfter') }}
</a-button>
<a-button <a-button
type="primary" type="primary"
danger danger
@@ -946,19 +984,9 @@ onMounted(() => {
{{ t('views.faultManage.activeAlarm.clear') }} {{ t('views.faultManage.activeAlarm.clear') }}
</a-button> </a-button>
<a-button type="primary" @click.prevent="fnShowSet()" v-if="false"> <a-button type="dashed" @click.prevent="fnExportList()">
<template #icon> <SettingOutlined /> </template> <template #icon><ExportOutlined /></template>
{{ t('views.faultManage.activeAlarm.disPlayFilfter') }} {{ t('common.export') }}
</a-button>
<a-button
type="primary"
@click.prevent="fnExportAll()"
:disabled="tableState.data.length <= 0"
v-if="false"
>
<template #icon> <export-outlined /> </template>
{{ t('views.faultManage.activeAlarm.exportAll') }}
</a-button> </a-button>
</a-space> </a-space>
</template> </template>

View File

@@ -7,11 +7,11 @@ import { SizeType } from 'ant-design-vue/es/config-provider';
import { MenuInfo } from 'ant-design-vue/es/menu/src/interface'; import { MenuInfo } from 'ant-design-vue/es/menu/src/interface';
import { ColumnsType } from 'ant-design-vue/es/table'; import { ColumnsType } from 'ant-design-vue/es/table';
import { import {
listAct,
updateConfirm, updateConfirm,
cancelConfirm, cancelConfirm,
exportAll, exportAll,
} from '@/api/faultManage/historyAlarm'; } from '@/api/faultManage/historyAlarm';
import { listAct, exportAlarm } from '@/api/faultManage/actAlarm';
import useI18n from '@/hooks/useI18n'; import useI18n from '@/hooks/useI18n';
import useDictStore from '@/store/modules/dict'; import useDictStore from '@/store/modules/dict';
import useNeInfoStore from '@/store/modules/neinfo'; import useNeInfoStore from '@/store/modules/neinfo';
@@ -59,9 +59,6 @@ let rangePickerPresets = ref([
}, },
]); ]);
/**表格字段列排序 */
let tableColumnsDnd = ref<ColumnsType>([]);
/**查询参数 */ /**查询参数 */
let queryParams = reactive({ let queryParams = reactive({
alarmStatus: 0, alarmStatus: 0,
@@ -144,7 +141,7 @@ let tableState: TabeStateType = reactive({
}); });
/**表格字段列 */ /**表格字段列 */
let tableColumns: ColumnsType = [ let tableColumns = ref<ColumnsType>([
{ {
title: t('views.faultManage.activeAlarm.alarmType'), title: t('views.faultManage.activeAlarm.alarmType'),
dataIndex: 'alarmType', dataIndex: 'alarmType',
@@ -241,7 +238,10 @@ let tableColumns: ColumnsType = [
fixed: 'right', fixed: 'right',
width: 100, width: 100,
}, },
]; ]);
/**表格字段列排序 */
let tableColumnsDnd = ref<ColumnsType>([]);
/**表格分页器参数 */ /**表格分页器参数 */
let tablePagination = reactive({ let tablePagination = reactive({
@@ -466,7 +466,7 @@ function mapKeysWithReduce(data: any[], titleMapping: Record<string, string>) {
function fnExportAll() { function fnExportAll() {
Modal.confirm({ Modal.confirm({
title: 'Tip', title: 'Tip',
content: t('views.faultManage.historyAlarm.exportSure'), content: t('views.faultManage.activeAlarm.exportSure'),
onOk() { onOk() {
const key = 'exportAlarmHis'; const key = 'exportAlarmHis';
message.loading({ content: t('common.loading'), key }); message.loading({ content: t('common.loading'), key });
@@ -543,6 +543,38 @@ function fnModalCancel() {
modalState.openByShowSet = false; modalState.openByShowSet = false;
} }
/**列表导出 */
function fnExportList() {
if (modalState.confirmLoading) return;
Modal.confirm({
title: t('common.tipTitle'),
content: t('views.faultManage.activeAlarm.exportTip'),
onOk() {
const hide = message.loading(t('common.loading'), 0);
const querys = toRaw(queryParams);
exportAlarm(querys)
.then(res => {
if (res.code === RESULT_CODE_SUCCESS) {
message.success({
content: t('common.operateOk'),
duration: 3,
});
saveAs(res.data, `history_alarm_export_${Date.now()}.xlsx`);
} else {
message.error({
content: `${res.msg}`,
duration: 3,
});
}
})
.finally(() => {
hide();
modalState.confirmLoading = false;
});
},
});
}
/**查询列表, pageNum初始页数 */ /**查询列表, pageNum初始页数 */
function fnGetList(pageNum?: number) { function fnGetList(pageNum?: number) {
if (tableState.loading) return; if (tableState.loading) return;
@@ -712,11 +744,17 @@ onMounted(() => {
<a-card :bordered="false" :body-style="{ padding: '0px' }"> <a-card :bordered="false" :body-style="{ padding: '0px' }">
<!-- 插槽-卡片左侧侧 --> <!-- 插槽-卡片左侧侧 -->
<template #title> <template #title>
<a-space :size="8" align="center" v-if="false"> <a-space :size="8" align="center">
<a-button type="dashed" @click.prevent="fnExportList()">
<template #icon><ExportOutlined /></template>
{{ t('common.export') }}
</a-button>
<a-button <a-button
type="primary" type="primary"
@click.prevent="fnCancelConfirm()" @click.prevent="fnCancelConfirm()"
:disabled="state.selectedRowKeys.length <= 0" :disabled="state.selectedRowKeys.length <= 0"
v-if="false"
> >
<template #icon> <template #icon>
<CloseOutlined /> <CloseOutlined />
@@ -727,6 +765,7 @@ onMounted(() => {
type="primary" type="primary"
@click.prevent="fnExportAll()" @click.prevent="fnExportAll()"
:disabled="tableState.data.length <= 0" :disabled="tableState.data.length <= 0"
v-if="false"
> >
<template #icon> <export-outlined /> </template> <template #icon> <export-outlined /> </template>
{{ t('views.faultManage.activeAlarm.exportAll') }} {{ t('views.faultManage.activeAlarm.exportAll') }}
@@ -796,28 +835,28 @@ onMounted(() => {
:scroll="{ x: tableColumns.length * 150, y: 'calc(100vh - 480px)' }" :scroll="{ x: tableColumns.length * 150, y: 'calc(100vh - 480px)' }"
> >
<template #bodyCell="{ column, record }"> <template #bodyCell="{ column, record }">
<template v-if="column.key === 'origSeverity'"> <template v-if="column?.key === 'origSeverity'">
<DictTag <DictTag
:options="dict.activeAlarmSeverity" :options="dict.activeAlarmSeverity"
:value="record.origSeverity" :value="record.origSeverity"
/> />
</template> </template>
<template v-if="column.key === 'alarmType'"> <template v-if="column?.key === 'alarmType'">
<DictTag <DictTag
:options="dict.activeAlarmType" :options="dict.activeAlarmType"
:value="record.alarmType" :value="record.alarmType"
/> />
</template> </template>
<template v-if="column.key === 'clearType'"> <template v-if="column?.key === 'clearType'">
<DictTag <DictTag
:options="dict.activeClearType" :options="dict.activeClearType"
:value="record.clearType" :value="record.clearType"
/> />
</template> </template>
<template v-if="column.key === 'ackState'"> <template v-if="column?.key === 'ackState'">
<DictTag :options="dict.activeAckState" :value="record.ackState" /> <DictTag :options="dict.activeAckState" :value="record.ackState" />
</template> </template>
<template v-if="column.key === 'id'"> <template v-if="column?.key === 'id'">
<a-space :size="8" align="center"> <a-space :size="8" align="center">
<a-tooltip> <a-tooltip>
<template #title>{{ t('common.viewText') }}</template> <template #title>{{ t('common.viewText') }}</template>

View File

@@ -1,38 +1,57 @@
<script setup lang="ts"> <script setup lang="ts">
import { reactive, ref, onMounted, toRaw } from 'vue'; import { reactive, ref, toRaw } from 'vue';
import { PageContainer } from 'antdv-pro-layout'; import { PageContainer } from 'antdv-pro-layout';
import { ProModal } from 'antdv-pro-modal';
import { SizeType } from 'ant-design-vue/es/config-provider'; import { SizeType } from 'ant-design-vue/es/config-provider';
import { ColumnsType } from 'ant-design-vue/es/table'; import { ColumnsType } from 'ant-design-vue/es/table';
import { Form, Modal, message } from 'ant-design-vue/es'; import { Modal, message } from 'ant-design-vue/es';
import BackupModal from '@/views/ne/neConfigBackup/components/BackupModal.vue';
import { parseDateToStr } from '@/utils/date-utils'; import { parseDateToStr } from '@/utils/date-utils';
import {
getBakFile,
getBakFileList,
downFile,
delFile,
updateFTPInfo,
getFTPInfo,
putFTPInfo,
} from '@/api/logManage/exportFile';
import { RESULT_CODE_SUCCESS } from '@/constants/result-constants'; import { RESULT_CODE_SUCCESS } from '@/constants/result-constants';
import useI18n from '@/hooks/useI18n'; import useI18n from '@/hooks/useI18n';
import saveAs from 'file-saver'; import saveAs from 'file-saver';
import { regExpIPv4 } from '@/utils/regular-utils'; import { delFile, getFile, listFile } from '@/api/tool/file';
import { parseSizeFromFile } from '@/utils/parse-utils';
import { pushBackupFTP } from '@/api/neData/backup';
const { t } = useI18n(); const { t } = useI18n();
/**网元参数 */ /**文件来源 */
let logSelect = ref<string[]>([]); let sourceState = reactive({
/**文件列表 */
/**文件列表 */ list: [
let fileList = ref<any>([]); {
value: '/log/operate_log',
label: t('views.logManage.exportFile.operateLog'),
path: '/usr/local/omc/backup',
},
{
value: '/cdr/ims_cdr',
label: t('views.logManage.exportFile.cdrIMS'),
path: '/usr/local/omc/backup',
},
{
value: '/cdr/smf_cdr',
label: t('views.logManage.exportFile.cdrSMF'),
path: '/usr/local/omc/backup',
},
{
value: '/cdr/smsc_cdr',
label: t('views.logManage.exportFile.cdrSMSC'),
path: '/usr/local/omc/backup',
},
{
value: '/cdr/sgwc_cdr',
label: t('views.logManage.exportFile.cdrSGWC'),
path: '/usr/local/omc/backup',
},
],
/**选择value */
value: undefined,
});
/**查询参数 */ /**查询参数 */
let queryParams = reactive({ let queryParams = reactive({
/**读取路径 */ /**读取路径 */
path: '', path: '',
/**表名 */
tableName: '',
/**当前页数 */ /**当前页数 */
pageNum: 1, pageNum: 1,
/**每页条数 */ /**每页条数 */
@@ -80,6 +99,9 @@ let tableColumns: ColumnsType = [
title: t('views.logManage.neFile.size'), title: t('views.logManage.neFile.size'),
dataIndex: 'size', dataIndex: 'size',
align: 'left', align: 'left',
customRender(opt) {
return parseSizeFromFile(opt.value);
},
width: 100, width: 100,
}, },
{ {
@@ -88,9 +110,9 @@ let tableColumns: ColumnsType = [
align: 'left', align: 'left',
customRender(opt) { customRender(opt) {
if (!opt.value) return ''; if (!opt.value) return '';
return parseDateToStr(opt.value * 1000); return parseDateToStr(opt.value);
}, },
width: 150, width: 200,
}, },
{ {
title: t('views.logManage.neFile.fileName'), title: t('views.logManage.neFile.fileName'),
@@ -135,10 +157,6 @@ let tablePagination = reactive({
/**下载触发等待 */ /**下载触发等待 */
let downLoading = ref<boolean>(false); let downLoading = ref<boolean>(false);
/**删除触发等待 */
let delLoading = ref<boolean>(false);
/**信息文件下载 */ /**信息文件下载 */
function fnDownloadFile(row: Record<string, any>) { function fnDownloadFile(row: Record<string, any>) {
if (downLoading.value) return; if (downLoading.value) return;
@@ -150,10 +168,7 @@ function fnDownloadFile(row: Record<string, any>) {
onOk() { onOk() {
downLoading.value = true; downLoading.value = true;
const hide = message.loading(t('common.loading'), 0); const hide = message.loading(t('common.loading'), 0);
downFile({ getFile(queryParams.path, row.fileName)
path: queryParams.path,
fileName: row.fileName,
})
.then(res => { .then(res => {
if (res.code === RESULT_CODE_SUCCESS) { if (res.code === RESULT_CODE_SUCCESS) {
message.success({ message.success({
@@ -178,6 +193,9 @@ function fnDownloadFile(row: Record<string, any>) {
}); });
} }
/**删除触发等待 */
let delLoading = ref<boolean>(false);
/**信息文件删除 */
function fnRecordDelete(row: Record<string, any>) { function fnRecordDelete(row: Record<string, any>) {
if (delLoading.value) return; if (delLoading.value) return;
Modal.confirm({ Modal.confirm({
@@ -186,30 +204,27 @@ function fnRecordDelete(row: Record<string, any>) {
fileName: row.fileName, fileName: row.fileName,
}), }),
onOk() { onOk() {
const key = 'delFile';
delLoading.value = true; delLoading.value = true;
message.loading({ content: t('common.loading'), key }); const hide = message.loading(t('common.loading'), 0);
delFile({ delFile(queryParams.path, row.fileName)
fileName: row.fileName,
path: queryParams.path,
})
.then(res => { .then(res => {
if (res.code === RESULT_CODE_SUCCESS) { if (res.code === RESULT_CODE_SUCCESS) {
message.success({ message.success({
content: t('views.system.user.delSuss'), content: t('common.msgSuccess', {
key, msg: t('common.deleteText'),
}),
duration: 2, duration: 2,
}); });
fnGetList(); fnGetList();
} else { } else {
message.error({ message.error({
content: `${res.msg}`, content: t('views.logManage.exportFile.deleteTipErr'),
key: key,
duration: 2, duration: 2,
}); });
} }
}) })
.finally(() => { .finally(() => {
hide();
delLoading.value = false; delLoading.value = false;
}); });
}, },
@@ -217,17 +232,18 @@ function fnRecordDelete(row: Record<string, any>) {
} }
/**网元类型选择对应修改 */ /**网元类型选择对应修改 */
function fnNeChange(keys: any, opt: any) { function fnNeChange(_: any, opt: any) {
queryParams.tableName = keys; queryParams.path = `${opt.path}${opt.value}`;
queryParams.path = opt.path; ftpInfo.path = queryParams.path;
ftpInfo.tag = opt.value;
fnGetList(1); fnGetList(1);
} }
/**查询备份信息列表, pageNum初始页数 */ /**查询备份信息列表, pageNum初始页数 */
function fnGetList(pageNum?: number) { function fnGetList(pageNum?: number) {
if (queryParams.tableName === '') { if (queryParams.path === '') {
message.warning({ message.warning({
content: t('views.logManage.exportFile.selectTip'), content: t('views.logManage.exportFile.fileSourcePlease'),
duration: 2, duration: 2,
}); });
return; return;
@@ -237,7 +253,7 @@ function fnGetList(pageNum?: number) {
if (pageNum) { if (pageNum) {
queryParams.pageNum = pageNum; queryParams.pageNum = pageNum;
} }
getBakFileList(toRaw(queryParams)).then(res => { listFile(toRaw(queryParams)).then(res => {
if (res.code === RESULT_CODE_SUCCESS) { if (res.code === RESULT_CODE_SUCCESS) {
const { total, rows } = res.data; const { total, rows } = res.data;
tablePagination.total = total; tablePagination.total = total;
@@ -259,147 +275,34 @@ function fnGetList(pageNum?: number) {
}); });
} }
onMounted(() => { /**打开FTP配置窗口 */
getBakFile().then(res => { const openFTPModal = ref<boolean>(false);
if (res.code === RESULT_CODE_SUCCESS) { function fnFTPModalOpen() {
res.data.forEach((item: any) => { openFTPModal.value = !openFTPModal.value;
fileList.value.push({ }
value: item.tableName,
label: item.tableDisplay,
path: item.filePath,
});
});
}
});
// .finally(() => {
// fnGetList();
// });
});
/**对象信息状态类型 */ type FTPInfoType = {
type ModalStateType = { path: string;
/**新增框或修改框是否显示 */ tag: string;
openByEdit: boolean; fileName: string;
/**标题 */
title: string;
/**表单数据 */
from: Record<string, any>;
/**确定按钮 loading */
confirmLoading: boolean;
}; };
const ftpInfo = reactive<FTPInfoType>({
/**FTP日志对象信息状态 */ path: '',
let modalState: ModalStateType = reactive({ tag: '',
openByEdit: false, fileName: '',
title: '设置远程备份配置',
from: {
username: '',
password: '',
toIp: '',
toPort: 22,
enable: false,
dir: '',
},
confirmLoading: false,
}); });
/**FTP日志对象信息内表单属性和校验规则 */ /**同步文件到FTP */
const modalStateFrom = Form.useForm( function fnSyncFileToFTP(fileName: string) {
modalState.from, ftpInfo.fileName = fileName;
reactive({ pushBackupFTP(toRaw(ftpInfo)).then(res => {
toIp: [ if (res.code === RESULT_CODE_SUCCESS) {
{ message.success(t('common.operateOk'), 3);
required: true,
pattern: regExpIPv4,
message: 'Please enter the service login IP',
},
],
username: [
{
required: true,
trigger: 'blur',
message: 'Please enter the service login user name',
},
],
dir: [
{
required: true,
trigger: 'blur',
message: 'Please enter the service address target file directory',
},
],
})
);
/**
* 对话框弹出显示为 新增或者修改
* @param configId 参数编号id, 不传为新增
*/
function fnModalVisibleByEdit() {
if (modalState.confirmLoading) return;
const hide = message.loading(t('common.loading'), 0);
modalState.confirmLoading = true;
getFTPInfo().then(res => {
modalState.confirmLoading = false;
hide();
if (res.code === RESULT_CODE_SUCCESS && res.data) {
modalState.from = Object.assign(modalState.from, res.data);
modalState.title = 'Setting Remote Backup';
modalState.openByEdit = true;
} else { } else {
message.error(res.msg, 3); message.warning(res.msg, 3);
modalState.title = 'Setting Remote Backup';
modalState.openByEdit = false;
} }
}); });
} }
/**FTP对象保存 */
function fnModalOk() {
modalStateFrom.validate().then(() => {
modalState.confirmLoading = true;
const from = toRaw(modalState.from);
updateFTPInfo(from)
.then(res => {
if (res.code === RESULT_CODE_SUCCESS) {
message.success(`Configuration saved successfully`, 3);
fnModalCancel();
} else {
message.warning(`Configuration save exception`, 3);
}
})
.finally(() => {
modalState.confirmLoading = false;
});
});
}
/**
* 对话框弹出关闭执行函数
* 进行表达规则校验
*/
function fnModalCancel() {
modalState.openByEdit = false;
modalStateFrom.resetFields();
}
/**
* 同步文件到FTP
* @param row
*/
function fnSyncFileToFTP(row: Record<string, any>) {
putFTPInfo(row.filePath, row.fileName)
.then(res => {
if (res.code === RESULT_CODE_SUCCESS) {
message.success(t('common.operateOk'), 3);
} else {
message.warning(res.msg, 3);
}
})
.finally(() => {
modalState.confirmLoading = false;
});
}
</script> </script>
<template> <template>
@@ -410,12 +313,14 @@ function fnSyncFileToFTP(row: Record<string, any>) {
<a-form :model="queryParams" name="queryParams" layout="horizontal"> <a-form :model="queryParams" name="queryParams" layout="horizontal">
<a-row :gutter="16" align="middle"> <a-row :gutter="16" align="middle">
<a-col> <a-col>
<span>{{ t('views.logManage.exportFile.fileName') }}:</span>&nbsp; <span>{{ t('views.logManage.exportFile.fileSource') }}:</span
>&nbsp;
<a-select <a-select
v-model:value="logSelect" v-model:value="sourceState.value"
:options="fileList" :options="sourceState.list"
@change="fnNeChange" @change="fnNeChange"
:allow-clear="false" :allow-clear="false"
:placeholder="t('common.selectPlease')"
style="width: 200px" style="width: 200px"
/> />
</a-col> </a-col>
@@ -436,9 +341,11 @@ function fnSyncFileToFTP(row: Record<string, any>) {
<!-- 插槽-卡片右侧 --> <!-- 插槽-卡片右侧 -->
<template #extra> <template #extra>
<a-space :size="8" align="center"> <a-space :size="8" align="center">
<a-tooltip> <a-tooltip placement="topRight">
<template #title>Setting Remote Backup</template> <template #title>
<a-button type="text" @click.prevent="fnModalVisibleByEdit()"> {{ t('views.ne.neConfigBackup.backupModal.title') }}
</template>
<a-button type="text" @click.prevent="fnFTPModalOpen()">
<template #icon><DeliveredProcedureOutlined /></template> <template #icon><DeliveredProcedureOutlined /></template>
</a-button> </a-button>
</a-tooltip> </a-tooltip>
@@ -465,135 +372,45 @@ function fnSyncFileToFTP(row: Record<string, any>) {
<template #bodyCell="{ column, record }"> <template #bodyCell="{ column, record }">
<template v-if="column.key === 'fileName'"> <template v-if="column.key === 'fileName'">
<a-space :size="8" align="center"> <a-space :size="8" align="center">
<a-button <a-tooltip placement="topRight" v-if="record.fileType === 'file'">
type="link" <template #title>
:loading="downLoading" {{ t('views.ne.neConfigBackup.backupModal.pushFileOper') }}
@click.prevent="fnSyncFileToFTP(record)" </template>
v-if="record.fileType === 'file'" <a-button
> type="link"
<template #icon><CloudServerOutlined /></template> @click.prevent="fnSyncFileToFTP(record.fileName)"
</a-button> >
<a-button <template #icon><CloudServerOutlined /></template>
type="link" </a-button>
:loading="downLoading" </a-tooltip>
@click.prevent="fnDownloadFile(record)" <a-tooltip placement="topRight" v-if="record.fileType === 'file'">
v-if="record.fileType === 'file'" <template #title>{{ t('common.downloadText') }}</template>
> <a-button
<template #icon><DownloadOutlined /></template> type="link"
</a-button> :loading="downLoading"
<a-button @click.prevent="fnDownloadFile(record)"
type="link" >
:loading="delLoading" <template #icon><DownloadOutlined /></template>
@click.prevent="fnRecordDelete(record)" </a-button>
v-if="record.fileType === 'file'" </a-tooltip>
> <a-tooltip placement="topRight" v-if="record.fileType === 'file'">
<template #icon><DeleteOutlined /></template> <template #title>{{ t('common.deleteText') }}</template>
</a-button> <a-button
type="link"
:loading="delLoading"
@click.prevent="fnRecordDelete(record)"
>
<template #icon><DeleteOutlined /></template>
</a-button>
</a-tooltip>
</a-space> </a-space>
</template> </template>
</template> </template>
</a-table> </a-table>
</a-card> </a-card>
<!-- 新增框或修改框 --> <!-- FTP配置窗口 -->
<ProModal <BackupModal v-model:open="openFTPModal"></BackupModal>
:drag="true"
:width="800"
:destroyOnClose="true"
:keyboard="false"
:mask-closable="false"
:open="modalState.openByEdit"
:title="modalState.title"
:confirm-loading="modalState.confirmLoading"
@ok="fnModalOk"
@cancel="fnModalCancel"
>
<a-form
name="modalStateFrom"
layout="horizontal"
:label-col="{ span: 6 }"
:label-wrap="true"
>
<a-form-item label="Enable" name="enable" :label-col="{ span: 3 }">
<a-switch
v-model:checked="modalState.from.enable"
:checked-children="t('common.switch.open')"
:un-checked-children="t('common.switch.shut')"
/>
</a-form-item>
<template v-if="modalState.from.enable">
<a-row>
<a-col :lg="12" :md="12" :xs="24">
<a-form-item
label="Service IP"
name="toIp"
v-bind="modalStateFrom.validateInfos.toIp"
>
<a-input
v-model:value="modalState.from.toIp"
allow-clear
:placeholder="t('common.inputPlease')"
></a-input>
</a-form-item>
</a-col>
<a-col :lg="12" :md="12" :xs="24">
<a-form-item
label="Service Port"
name="toPort"
v-bind="modalStateFrom.validateInfos.toPort"
>
<a-input-number
v-model:value="modalState.from.toPort"
allow-clear
:placeholder="t('common.inputPlease')"
></a-input-number>
</a-form-item>
</a-col>
</a-row>
<a-row>
<a-col :lg="12" :md="12" :xs="24">
<a-form-item
label="UserName"
name="username"
v-bind="modalStateFrom.validateInfos.username"
>
<a-input
v-model:value="modalState.from.username"
allow-clear
:placeholder="t('common.inputPlease')"
></a-input>
</a-form-item>
</a-col>
<a-col :lg="12" :md="12" :xs="24">
<a-form-item
label="Password"
name="password"
v-bind="modalStateFrom.validateInfos.password"
>
<a-input-password
v-model:value="modalState.from.password"
allow-clear
:placeholder="t('common.inputPlease')"
></a-input-password>
</a-form-item>
</a-col>
</a-row>
<a-form-item
label="Save Dir"
name="dir"
v-bind="modalStateFrom.validateInfos.dir"
:label-col="{ span: 3 }"
>
<a-input
v-model:value="modalState.from.dir"
allow-clear
:placeholder="t('common.inputPlease')"
></a-input>
</a-form-item>
</template>
</a-form>
</ProModal>
</PageContainer> </PageContainer>
</template> </template>

View File

@@ -97,7 +97,7 @@ let tableColumns: ColumnsType = reactive([
title: t('views.logManage.mml.logTime'), title: t('views.logManage.mml.logTime'),
dataIndex: 'logTime', dataIndex: 'logTime',
align: 'left', align: 'left',
width: 150, width: 200,
customRender(opt) { customRender(opt) {
if (!opt.value) return ''; if (!opt.value) return '';
return parseDateToStr(opt.value); return parseDateToStr(opt.value);

View File

@@ -5,7 +5,7 @@ import { message } from 'ant-design-vue/es';
import { reactive, onMounted, computed, toRaw } from 'vue'; import { reactive, onMounted, computed, toRaw } from 'vue';
import useUserStore from '@/store/modules/user'; import useUserStore from '@/store/modules/user';
import useAppStore from '@/store/modules/app'; import useAppStore from '@/store/modules/app';
import { getCaptchaImage } from '@/api/login'; import { getCaptchaImage } from '@/api/auth';
import { useRouter, useRoute } from 'vue-router'; import { useRouter, useRoute } from 'vue-router';
import useI18n from '@/hooks/useI18n'; import useI18n from '@/hooks/useI18n';
import useLayoutStore from '@/store/modules/layout'; import useLayoutStore from '@/store/modules/layout';
@@ -75,18 +75,28 @@ function fnFinish() {
function fnGetCaptcha() { function fnGetCaptcha() {
if (state.captchaClick) return; if (state.captchaClick) return;
state.captchaClick = true; state.captchaClick = true;
getCaptchaImage().then(res => { getCaptchaImage()
state.captchaClick = false; .then(res => {
if (res.code != 1) { if (res.code !== RESULT_CODE_SUCCESS) {
message.warning(`${res.msg}`, 3); message.warning({
return; content: `${res.msg}`,
} duration: 3,
state.captcha.enabled = Boolean(res.captchaEnabled); });
if (state.captcha.enabled) { return;
state.captcha.codeImg = res.img; }
state.from.uuid = res.uuid; const { enabled, img, uuid } = res.data;
} state.captcha.enabled = Boolean(enabled);
}); if (state.captcha.enabled) {
state.captcha.codeImg = img;
state.from.uuid = uuid;
}
if (res.data?.text) {
state.from.code = res.data.text;
}
})
.finally(() => {
state.captchaClick = false;
});
} }
// 判断是否有背景地址 // 判断是否有背景地址

View File

@@ -181,9 +181,9 @@ function fnSendMML() {
if (res.code === RESULT_CODE_SUCCESS) { if (res.code === RESULT_CODE_SUCCESS) {
let resultArr = res.data; let resultArr = res.data;
for (let i = 0; i < resultArr.length; i++) { for (let i = 0; i < resultArr.length; i++) {
const str = resultArr[i]; const str = resultArr[i] || '';
const logStr = str.replace(/(\r\n|\n)/g, '\n'); const logStr = str.replace(/(\r\n|\n)/g, '\n');
const cmdStr = cmdArr[i]; const cmdStr = cmdArr[i] || '';
state.mmlCmdLog += `${cmdStr}\n${logStr}\n`; state.mmlCmdLog += `${cmdStr}\n${logStr}\n`;
} }
} else { } else {

View File

@@ -183,9 +183,9 @@ function fnSendMML() {
if (res.code === RESULT_CODE_SUCCESS) { if (res.code === RESULT_CODE_SUCCESS) {
let resultArr = res.data; let resultArr = res.data;
for (let i = 0; i < resultArr.length; i++) { for (let i = 0; i < resultArr.length; i++) {
const str = resultArr[i]; const str = resultArr[i] || '';
const logStr = str.replace(/(\r\n|\n)/g, '\n'); const logStr = str.replace(/(\r\n|\n)/g, '\n');
const cmdStr = cmdArr[i]; const cmdStr = cmdArr[i] || '';
state.mmlCmdLog += `${cmdStr}\n${logStr}\n`; state.mmlCmdLog += `${cmdStr}\n${logStr}\n`;
} }
} else { } else {

View File

@@ -183,9 +183,9 @@ function fnSendMML() {
if (res.code === RESULT_CODE_SUCCESS) { if (res.code === RESULT_CODE_SUCCESS) {
let resultArr = res.data; let resultArr = res.data;
for (let i = 0; i < resultArr.length; i++) { for (let i = 0; i < resultArr.length; i++) {
const str = resultArr[i] || ""; const str = resultArr[i] || '';
const logStr = str.replace(/(\r\n|\n)/g, '\n'); const logStr = str.replace(/(\r\n|\n)/g, '\n');
const cmdStr = cmdArr[i] || ""; const cmdStr = cmdArr[i] || '';
state.mmlCmdLog += `${cmdStr}\n${logStr}\n`; state.mmlCmdLog += `${cmdStr}\n${logStr}\n`;
} }
} else { } else {

View File

@@ -1100,7 +1100,7 @@ onMounted(() => {
<a-textarea <a-textarea
v-model:value="modalState.from.targetParams" v-model:value="modalState.from.targetParams"
:auto-size="{ minRows: 2, maxRows: 6 }" :auto-size="{ minRows: 2, maxRows: 6 }"
:maxlength="400" :maxlength="2000"
:placeholder="t('views.monitor.job.targetParamsPlease')" :placeholder="t('views.monitor.job.targetParamsPlease')"
/> />
</a-form-item> </a-form-item>

View File

@@ -118,8 +118,8 @@ let tableState: TabeStateType = reactive({
let tableColumns: ColumnsType = [ let tableColumns: ColumnsType = [
{ {
title: t('common.rowId'), title: t('common.rowId'),
dataIndex: 'jobLogId', dataIndex: 'logId',
align: 'center', align: 'left',
width: 100, width: 100,
}, },
{ {
@@ -132,7 +132,7 @@ let tableColumns: ColumnsType = [
title: t('views.monitor.jobLog.jobGroup'), title: t('views.monitor.jobLog.jobGroup'),
dataIndex: 'jobGroup', dataIndex: 'jobGroup',
key: 'jobGroup', key: 'jobGroup',
align: 'center', align: 'left',
width: 100, width: 100,
}, },
{ {
@@ -142,17 +142,17 @@ let tableColumns: ColumnsType = [
width: 100, width: 100,
}, },
{ {
title: t('views.monitor.jobLog.statusFlag'), title: t('views.monitor.jobLog.status'),
dataIndex: 'statusFlag', dataIndex: 'statusFlag',
key: 'statusFlag', key: 'statusFlag',
align: 'center', align: 'left',
width: 100, width: 100,
}, },
{ {
title: t('views.monitor.jobLog.createTime'), title: t('views.monitor.jobLog.createTime'),
dataIndex: 'createTime', dataIndex: 'createTime',
align: 'center', align: 'left',
width: 150, width: 200,
customRender(opt) { customRender(opt) {
if (+opt.value <= 0) return ''; if (+opt.value <= 0) return '';
return parseDateToStr(+opt.value); return parseDateToStr(+opt.value);
@@ -170,7 +170,7 @@ let tableColumns: ColumnsType = [
}, },
{ {
title: t('common.operate'), title: t('common.operate'),
key: 'jobLogId', key: 'logId',
align: 'left', align: 'left',
}, },
]; ];
@@ -229,7 +229,7 @@ let modalState: ModalStateType = reactive({
open: false, open: false,
title: '任务日志', title: '任务日志',
from: { from: {
jobLogId: undefined, logId: undefined,
jobName: '', jobName: '',
jobGroup: 'DEFAULT', jobGroup: 'DEFAULT',
invokeTarget: '', invokeTarget: '',
@@ -588,7 +588,7 @@ onMounted(() => {
<!-- 表格列表 --> <!-- 表格列表 -->
<a-table <a-table
class="table" class="table"
row-key="jobLogId" row-key="logId"
:columns="tableColumns" :columns="tableColumns"
:loading="tableState.loading" :loading="tableState.loading"
:data-source="tableState.data" :data-source="tableState.data"
@@ -615,7 +615,7 @@ onMounted(() => {
}} }}
</a-tag> </a-tag>
</template> </template>
<template v-if="column.key === 'jobLogId'"> <template v-if="column.key === 'logId'">
<a-space :size="8" align="center"> <a-space :size="8" align="center">
<a-tooltip> <a-tooltip>
<template #title>{{ t('common.viewText') }}</template> <template #title>{{ t('common.viewText') }}</template>
@@ -644,8 +644,8 @@ onMounted(() => {
<a-form layout="horizontal" :label-col="{ span: 6 }" :label-wrap="true"> <a-form layout="horizontal" :label-col="{ span: 6 }" :label-wrap="true">
<a-row> <a-row>
<a-col :lg="12" :md="12" :xs="24"> <a-col :lg="12" :md="12" :xs="24">
<a-form-item :label="t('common.rowId')" name="jobLogId"> <a-form-item :label="t('common.rowId')" name="logId">
{{ modalState.from.jobLogId }} {{ modalState.from.logId }}
</a-form-item> </a-form-item>
</a-col> </a-col>
<a-col :lg="12" :md="12" :xs="24"> <a-col :lg="12" :md="12" :xs="24">

View File

@@ -0,0 +1,230 @@
<script setup lang="ts">
import { reactive, watch, toRaw } from 'vue';
import { ProModal } from 'antdv-pro-modal';
import useI18n from '@/hooks/useI18n';
import { Form, message } from 'ant-design-vue';
import { regExpIPv4 } from '@/utils/regular-utils';
import { getBackupFTP, updateBackupFTP } from '@/api/neData/backup';
import { RESULT_CODE_SUCCESS } from '@/constants/result-constants';
const { t } = useI18n();
const emit = defineEmits(['ok', 'cancel', 'update:open']);
const props = defineProps({
open: {
type: Boolean,
default: false,
required: true,
},
});
/**对话框对象信息状态类型 */
type StateType = {
/**框是否显示 */
open: boolean;
/**标题 */
title: string;
/**查看命令 */
form: Record<string, any>;
/**等待 */
confirmLoading: boolean;
};
/**对话框对象信息状态 */
let state: StateType = reactive({
open: false,
title: '设置远程备份配置',
form: {
username: '',
password: '',
toIp: '',
toPort: 22,
enable: false,
dir: '',
},
confirmLoading: false,
});
/**FTP对象信息内表单属性和校验规则 */
const stateFrom = Form.useForm(
state.form,
reactive({
toIp: [
{
required: true,
pattern: regExpIPv4,
message: t('views.ne.neConfigBackup.backupModal.toIpPleace'),
},
],
username: [
{
required: true,
trigger: 'blur',
message: t('views.ne.neConfigBackup.backupModal.usernamePleace'),
},
],
dir: [
{
required: true,
trigger: 'blur',
message: t('views.ne.neConfigBackup.backupModal.dirPleace'),
},
],
})
);
/**对象保存 */
function fnModalOk() {
stateFrom.validate().then(() => {
state.confirmLoading = true;
const from = toRaw(state.form);
updateBackupFTP(from)
.then(res => {
if (res.code === RESULT_CODE_SUCCESS) {
message.success(t('common.operateOk'), 3);
fnModalCancel();
} else {
message.warning(t('common.operateErr'), 3);
}
})
.finally(() => {
state.confirmLoading = false;
});
});
}
/**
* 对话框弹出关闭执行函数
* 进行表达规则校验
*/
function fnModalCancel() {
stateFrom.resetFields();
state.open = false;
emit('cancel');
emit('update:open', false);
}
/**
* 对话框弹出显示为 新增或者修改
*/
function fnModalVisible() {
if (state.confirmLoading) return;
const hide = message.loading(t('common.loading'), 0);
state.confirmLoading = true;
getBackupFTP()
.then(res => {
if (res.code === RESULT_CODE_SUCCESS && res.data) {
state.form = Object.assign(state.form, res.data);
}
})
.finally(() => {
state.confirmLoading = false;
hide();
state.title = t('views.ne.neConfigBackup.backupModal.title');
state.open = true;
});
}
/**监听是否显示,初始数据 */
watch(
() => props.open,
val => {
if (val) {
fnModalVisible();
}
}
);
</script>
<template>
<ProModal
:drag="true"
:width="520"
:destroyOnClose="true"
:keyboard="false"
:mask-closable="false"
:open="state.open"
:title="state.title"
:confirm-loading="state.confirmLoading"
@ok="fnModalOk"
@cancel="fnModalCancel"
>
<a-form
name="modalStateFTPFrom"
layout="horizontal"
:label-col="{ span: 6 }"
:label-wrap="true"
>
<a-form-item
:label="t('views.ne.neConfigBackup.backupModal.enable')"
name="enable"
>
<a-switch
v-model:checked="state.form.enable"
:checked-children="t('common.switch.open')"
:un-checked-children="t('common.switch.shut')"
/>
</a-form-item>
<template v-if="state.form.enable">
<a-form-item
:label="t('views.ne.neConfigBackup.backupModal.toIp')"
name="toIp"
v-bind="stateFrom.validateInfos.toIp"
>
<a-input
v-model:value="state.form.toIp"
allow-clear
:placeholder="t('common.inputPlease')"
></a-input>
</a-form-item>
<a-form-item
:label="t('views.ne.neConfigBackup.backupModal.toPort')"
name="toPort"
v-bind="stateFrom.validateInfos.toPort"
>
<a-input-number
v-model:value="state.form.toPort"
allow-clear
:placeholder="t('common.inputPlease')"
></a-input-number>
</a-form-item>
<a-form-item
:label="t('views.ne.neConfigBackup.backupModal.username')"
name="username"
v-bind="stateFrom.validateInfos.username"
>
<a-input
v-model:value="state.form.username"
allow-clear
:placeholder="t('common.inputPlease')"
></a-input>
</a-form-item>
<a-form-item
:label="t('views.ne.neConfigBackup.backupModal.password')"
name="password"
v-bind="stateFrom.validateInfos.password"
>
<a-input-password
v-model:value="state.form.password"
allow-clear
:placeholder="t('common.inputPlease')"
></a-input-password>
</a-form-item>
<a-form-item
:label="t('views.ne.neConfigBackup.backupModal.dir')"
name="dir"
v-bind="stateFrom.validateInfos.dir"
>
<a-input
v-model:value="state.form.dir"
allow-clear
:placeholder="t('common.inputPlease')"
></a-input>
</a-form-item>
</template>
</a-form>
</ProModal>
</template>
<style scoped></style>

View File

@@ -5,22 +5,20 @@ import { ProModal } from 'antdv-pro-modal';
import { Form, Modal, TableColumnsType, message } from 'ant-design-vue/es'; import { Form, Modal, TableColumnsType, message } from 'ant-design-vue/es';
import { SizeType } from 'ant-design-vue/es/config-provider'; import { SizeType } from 'ant-design-vue/es/config-provider';
import { MenuInfo } from 'ant-design-vue/es/menu/src/interface'; import { MenuInfo } from 'ant-design-vue/es/menu/src/interface';
import BackupModal from './components/BackupModal.vue';
import useNeInfoStore from '@/store/modules/neinfo'; import useNeInfoStore from '@/store/modules/neinfo';
import useI18n from '@/hooks/useI18n'; import useI18n from '@/hooks/useI18n';
import useDictStore from '@/store/modules/dict'; import useDictStore from '@/store/modules/dict';
import { NE_TYPE_LIST } from '@/constants/ne-constants'; import { NE_TYPE_LIST } from '@/constants/ne-constants';
import { RESULT_CODE_SUCCESS } from '@/constants/result-constants'; import { RESULT_CODE_SUCCESS } from '@/constants/result-constants';
import { parseDateToStr } from '@/utils/date-utils'; import { parseDateToStr } from '@/utils/date-utils';
import { regExpIPv4 } from '@/utils/regular-utils';
import { import {
delNeConfigBackup, delNeConfigBackup,
downNeConfigBackup, downNeConfigBackup,
listNeConfigBackup, listNeConfigBackup,
updateNeConfigBackup, updateNeConfigBackup,
getFTPInfo,
putFTPInfo,
updateFTPInfo,
} from '@/api/ne/neConfigBackup'; } from '@/api/ne/neConfigBackup';
import { pushBackupFTP } from '@/api/neData/backup';
import saveAs from 'file-saver'; import saveAs from 'file-saver';
const { t } = useI18n(); const { t } = useI18n();
const { getDict } = useDictStore(); const { getDict } = useDictStore();
@@ -389,119 +387,26 @@ onMounted(() => {
}); });
}); });
/**FTP日志对象信息状态 */ /**打开FTP配置窗口 */
let modalStateFTP: ModalStateType = reactive({ const openFTPModal = ref<boolean>(false);
openByEdit: false, function fnFTPModalOpen() {
title: '设置远程备份配置', openFTPModal.value = !openFTPModal.value;
from: { }
username: '',
password: '',
toIp: '',
toPort: 22,
enable: false,
dir: '',
},
confirmLoading: false,
});
/**FTP日志对象信息内表单属性和校验规则 */ /**同步文件到FTP */
const modalStateFTPFrom = Form.useForm( function fnSyncFileToFTP(row: Record<string, any>) {
modalStateFTP.from, pushBackupFTP({
reactive({ path: row.path.substring(0, row.path.lastIndexOf('/')),
toIp: [ fileName: row.name,
{ tag: 'ne_config',
required: true, }).then(res => {
pattern: regExpIPv4, if (res.code === RESULT_CODE_SUCCESS) {
message: 'Please enter the service login IP', message.success(t('common.operateOk'), 3);
},
],
username: [
{
required: true,
trigger: 'blur',
message: 'Please enter the service login user name',
},
],
dir: [
{
required: true,
trigger: 'blur',
message: 'Please enter the service address target file directory',
},
],
})
);
/**
* 对话框弹出显示为 新增或者修改
* @param configId 参数编号id, 不传为新增
*/
function fnModalFTPVisibleByEdit() {
if (modalStateFTP.confirmLoading) return;
const hide = message.loading(t('common.loading'), 0);
modalStateFTP.confirmLoading = true;
getFTPInfo().then(res => {
modalStateFTP.confirmLoading = false;
hide();
if (res.code === RESULT_CODE_SUCCESS && res.data) {
modalStateFTP.from = Object.assign(modalStateFTP.from, res.data);
modalStateFTP.title = 'Setting Remote Backup';
modalStateFTP.openByEdit = true;
} else { } else {
message.error(res.msg, 3); message.warning(res.msg, 3);
modalStateFTP.title = 'Setting Remote Backup';
modalStateFTP.openByEdit = false;
} }
}); });
} }
/**FTP对象保存 */
function fnModalFTPOk() {
modalStateFTPFrom.validate().then(() => {
modalStateFTP.confirmLoading = true;
const from = toRaw(modalStateFTP.from);
updateFTPInfo(from)
.then(res => {
if (res.code === RESULT_CODE_SUCCESS) {
message.success(`Configuration saved successfully`, 3);
fnModalFTPCancel();
} else {
message.warning(`Configuration save exception`, 3);
}
})
.finally(() => {
modalStateFTP.confirmLoading = false;
});
});
}
/**
* 对话框弹出关闭执行函数
* 进行表达规则校验
*/
function fnModalFTPCancel() {
modalStateFTP.openByEdit = false;
modalStateFTPFrom.resetFields();
}
/**
* 同步文件到FTP
* @param row
*/
function fnSyncFileToFTP(row: Record<string, any>) {
modalStateFTP.confirmLoading = true;
putFTPInfo(row.path)
.then(res => {
if (res.code === RESULT_CODE_SUCCESS) {
message.success(t('common.operateOk'), 3);
} else {
message.warning(res.msg, 3);
}
})
.finally(() => {
modalStateFTP.confirmLoading = false;
});
}
</script> </script>
<template> <template>
@@ -572,8 +477,10 @@ function fnSyncFileToFTP(row: Record<string, any>) {
<template #extra> <template #extra>
<a-space :size="8" align="center"> <a-space :size="8" align="center">
<a-tooltip placement="topRight"> <a-tooltip placement="topRight">
<template #title>Setting Remote Backup</template> <template #title>
<a-button type="text" @click.prevent="fnModalFTPVisibleByEdit()"> {{ t('views.ne.neConfigBackup.backupModal.title') }}
</template>
<a-button type="text" @click.prevent="fnFTPModalOpen()">
<template #icon><DeliveredProcedureOutlined /></template> <template #icon><DeliveredProcedureOutlined /></template>
</a-button> </a-button>
</a-tooltip> </a-tooltip>
@@ -632,12 +539,10 @@ function fnSyncFileToFTP(row: Record<string, any>) {
<template v-if="column.key === 'id'"> <template v-if="column.key === 'id'">
<a-space :size="8" align="center"> <a-space :size="8" align="center">
<a-tooltip placement="topRight"> <a-tooltip placement="topRight">
<template #title>Send Current File To Remote Backup</template> <template #title>
<a-button {{ t('views.ne.neConfigBackup.backupModal.pushFileOper') }}
type="link" </template>
:loading="modalStateFTP.confirmLoading" <a-button type="link" @click.prevent="fnSyncFileToFTP(record)">
@click.prevent="fnSyncFileToFTP(record)"
>
<template #icon><CloudServerOutlined /></template> <template #icon><CloudServerOutlined /></template>
</a-button> </a-button>
</a-tooltip> </a-tooltip>
@@ -676,7 +581,7 @@ function fnSyncFileToFTP(row: Record<string, any>) {
<!-- 新增框或修改框 --> <!-- 新增框或修改框 -->
<ProModal <ProModal
:drag="true" :drag="true"
:width="512" :width="520"
:destroyOnClose="true" :destroyOnClose="true"
:keyboard="false" :keyboard="false"
:mask-closable="false" :mask-closable="false"
@@ -719,105 +624,8 @@ function fnSyncFileToFTP(row: Record<string, any>) {
</a-form> </a-form>
</ProModal> </ProModal>
<!-- FTP --> <!-- FTP配置窗口 -->
<ProModal <BackupModal v-model:open="openFTPModal"></BackupModal>
:drag="true"
:width="800"
:destroyOnClose="true"
:keyboard="false"
:mask-closable="false"
:open="modalStateFTP.openByEdit"
:title="modalStateFTP.title"
:confirm-loading="modalStateFTP.confirmLoading"
@ok="fnModalFTPOk"
@cancel="fnModalFTPCancel"
>
<a-form
name="modalStateFTPFrom"
layout="horizontal"
:label-col="{ span: 6 }"
:label-wrap="true"
>
<a-form-item label="Enable" name="enable" :label-col="{ span: 3 }">
<a-switch
v-model:checked="modalStateFTP.from.enable"
:checked-children="t('common.switch.open')"
:un-checked-children="t('common.switch.shut')"
/>
</a-form-item>
<template v-if="modalStateFTP.from.enable">
<a-row>
<a-col :lg="12" :md="12" :xs="24">
<a-form-item
label="Service IP"
name="toIp"
v-bind="modalStateFTPFrom.validateInfos.toIp"
>
<a-input
v-model:value="modalStateFTP.from.toIp"
allow-clear
:placeholder="t('common.inputPlease')"
></a-input>
</a-form-item>
</a-col>
<a-col :lg="12" :md="12" :xs="24">
<a-form-item
label="Service Port"
name="toPort"
v-bind="modalStateFTPFrom.validateInfos.toPort"
>
<a-input-number
v-model:value="modalStateFTP.from.toPort"
allow-clear
:placeholder="t('common.inputPlease')"
></a-input-number>
</a-form-item>
</a-col>
</a-row>
<a-row>
<a-col :lg="12" :md="12" :xs="24">
<a-form-item
label="UserName"
name="username"
v-bind="modalStateFTPFrom.validateInfos.username"
>
<a-input
v-model:value="modalStateFTP.from.username"
allow-clear
:placeholder="t('common.inputPlease')"
></a-input>
</a-form-item>
</a-col>
<a-col :lg="12" :md="12" :xs="24">
<a-form-item
label="Password"
name="password"
v-bind="modalStateFTPFrom.validateInfos.password"
>
<a-input-password
v-model:value="modalStateFTP.from.password"
allow-clear
:placeholder="t('common.inputPlease')"
></a-input-password>
</a-form-item>
</a-col>
</a-row>
<a-form-item
label="Save Dir"
name="dir"
v-bind="modalStateFTPFrom.validateInfos.dir"
:label-col="{ span: 3 }"
>
<a-input
v-model:value="modalStateFTP.from.dir"
allow-clear
:placeholder="t('common.inputPlease')"
></a-input>
</a-form-item>
</template>
</a-form>
</ProModal>
</PageContainer> </PageContainer>
</template> </template>

View File

@@ -12,6 +12,7 @@ import useI18n from '@/hooks/useI18n';
import ViewDrawer from './components/ViewDrawer.vue'; import ViewDrawer from './components/ViewDrawer.vue';
import saveAs from 'file-saver'; import saveAs from 'file-saver';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import { parseSizeFromFile } from '@/utils/parse-utils';
const neInfoStore = useNeInfoStore(); const neInfoStore = useNeInfoStore();
const { t } = useI18n(); const { t } = useI18n();
const route = useRoute(); const route = useRoute();
@@ -79,16 +80,19 @@ let tableColumns: ColumnsType = [
dataIndex: 'size', dataIndex: 'size',
align: 'left', align: 'left',
width: 100, width: 100,
customRender(opt) {
return parseSizeFromFile(opt.value);
},
}, },
{ {
title: t('views.logManage.neFile.modifiedTime'), title: t('views.logManage.neFile.modifiedTime'),
dataIndex: 'modifiedTime', dataIndex: 'modifiedTime',
align: 'left', align: 'left',
width: 200,
customRender(opt) { customRender(opt) {
if (!opt.value) return ''; if (!opt.value) return '';
return parseDateToStr(opt.value * 1000); return parseDateToStr(opt.value * 1000);
}, },
width: 150,
}, },
{ {
title: t('views.logManage.neFile.fileName'), title: t('views.logManage.neFile.fileName'),
@@ -238,7 +242,7 @@ function fnGetList(pageNum?: number) {
} }
queryParams.path = nePathArr.value.join('/'); queryParams.path = nePathArr.value.join('/');
listNeFiles(toRaw(queryParams)).then(res => { listNeFiles(toRaw(queryParams)).then(res => {
if (res.code === RESULT_CODE_SUCCESS) { if (res.code === RESULT_CODE_SUCCESS) {
const { total, rows } = res.data; const { total, rows } = res.data;
tablePagination.total = total; tablePagination.total = total;
tableState.data = rows; tableState.data = rows;

View File

@@ -10,7 +10,6 @@ import { RESULT_CODE_SUCCESS } from '@/constants/result-constants';
import { delNeHost, listNeHost } from '@/api/ne/neHost'; import { delNeHost, listNeHost } from '@/api/ne/neHost';
import useDictStore from '@/store/modules/dict'; import useDictStore from '@/store/modules/dict';
import useI18n from '@/hooks/useI18n'; import useI18n from '@/hooks/useI18n';
import { number } from 'echarts';
const { getDict } = useDictStore(); const { getDict } = useDictStore();
const { t } = useI18n(); const { t } = useI18n();
const EditModal = defineAsyncComponent( const EditModal = defineAsyncComponent(

View File

@@ -4,7 +4,7 @@ import { ProModal } from 'antdv-pro-modal';
import { message, Form } from 'ant-design-vue/es'; import { message, Form } from 'ant-design-vue/es';
import useI18n from '@/hooks/useI18n'; import useI18n from '@/hooks/useI18n';
import { RESULT_CODE_SUCCESS } from '@/constants/result-constants'; import { RESULT_CODE_SUCCESS } from '@/constants/result-constants';
import { getOAMFile, saveOAMFile } from '@/api/ne/neInfo'; import { getOAMFile, saveOAMFile, serviceNeAction } from '@/api/ne/neInfo';
const { t } = useI18n(); const { t } = useI18n();
const emit = defineEmits(['ok', 'cancel', 'update:open']); const emit = defineEmits(['ok', 'cancel', 'update:open']);
const props = defineProps({ const props = defineProps({
@@ -29,8 +29,8 @@ type ModalStateType = {
openByEdit: boolean; openByEdit: boolean;
/**标题 */ /**标题 */
title: string; title: string;
/**是否同步 */ /**是否重启 */
sync: boolean; restart: boolean;
/**表单数据 */ /**表单数据 */
from: Record<string, any>; from: Record<string, any>;
/**确定按钮 loading */ /**确定按钮 loading */
@@ -41,7 +41,7 @@ type ModalStateType = {
let modalState: ModalStateType = reactive({ let modalState: ModalStateType = reactive({
openByEdit: false, openByEdit: false,
title: 'OAM Configuration', title: 'OAM Configuration',
sync: true, restart: false,
from: { from: {
omcIP: '', omcIP: '',
oamEnable: true, oamEnable: true,
@@ -114,12 +114,19 @@ function fnModalOk() {
neType: props.neType, neType: props.neType,
neId: props.neId, neId: props.neId,
content: from, content: from,
sync: modalState.sync, sync: true,
}) })
.then(res => { .then(res => {
if (res.code === RESULT_CODE_SUCCESS) { if (res.code === RESULT_CODE_SUCCESS) {
message.success(t('common.operateOk'), 3); message.success(t('common.operateOk'), 3);
emit('ok'); emit('ok');
if (modalState.restart) {
serviceNeAction({
neType: props.neType,
neId: props.neId,
action: 'restart',
});
}
fnModalCancel(); fnModalCancel();
} else { } else {
message.error({ message.error({
@@ -145,6 +152,7 @@ function fnModalOk() {
function fnModalCancel() { function fnModalCancel() {
modalState.openByEdit = false; modalState.openByEdit = false;
modalState.confirmLoading = false; modalState.confirmLoading = false;
modalState.restart = false;
modalStateFrom.resetFields(); modalStateFrom.resetFields();
emit('cancel'); emit('cancel');
emit('update:open', false); emit('update:open', false);
@@ -183,15 +191,15 @@ watch(
:labelWrap="true" :labelWrap="true"
> >
<a-form-item <a-form-item
:label="t('views.ne.neInfo.oam.sync')" :label="t('views.ne.neInfo.oam.restart')"
name="sync" name="restart"
:label-col="{ span: 6 }" :label-col="{ span: 6 }"
:labelWrap="true" :labelWrap="true"
> >
<a-switch <a-switch
:checked-children="t('common.switch.open')" :checked-children="t('common.switch.open')"
:un-checked-children="t('common.switch.shut')" :un-checked-children="t('common.switch.shut')"
v-model:checked="modalState.sync" v-model:checked="modalState.restart"
></a-switch> ></a-switch>
</a-form-item> </a-form-item>

View File

@@ -0,0 +1,412 @@
<script setup lang="ts">
import { reactive, ref, toRaw } from 'vue';
import { PageContainer } from 'antdv-pro-layout';
import { SizeType } from 'ant-design-vue/es/config-provider';
import { ColumnsType } from 'ant-design-vue/es/table';
import { Modal, message } from 'ant-design-vue/es';
import BackupModal from '@/views/ne/neConfigBackup/components/BackupModal.vue';
import { parseDateToStr } from '@/utils/date-utils';
import { RESULT_CODE_SUCCESS } from '@/constants/result-constants';
import useI18n from '@/hooks/useI18n';
import saveAs from 'file-saver';
import { delFile, getFile, listFile } from '@/api/tool/file';
import { parseSizeFromFile } from '@/utils/parse-utils';
import { pushBackupFTP } from '@/api/neData/backup';
const { t } = useI18n();
/**文件来源 */
let sourceState = reactive({
/**文件列表 */
list: [
{
value: '/auth',
label: t('views.neData.backupData.auth'),
path: '/usr/local/omc/backup/udm_data',
},
{
value: '/sub',
label: t('views.neData.backupData.sub'),
path: '/usr/local/omc/backup/udm_data',
},
{
value: '/voip',
label: t('views.neData.backupData.voip'),
path: '/usr/local/omc/backup/udm_data',
},
{
value: '/volte',
label: t('views.neData.backupData.volte'),
path: '/usr/local/omc/backup/udm_data',
},
],
/**选择value */
value: undefined,
});
/**查询参数 */
let queryParams = reactive({
/**读取路径 */
path: '',
/**当前页数 */
pageNum: 1,
/**每页条数 */
pageSize: 20,
});
/**表格状态类型 */
type TabeStateType = {
/**加载等待 */
loading: boolean;
/**紧凑型 */
size: SizeType;
/**记录数据 */
data: object[];
};
/**表格状态 */
let tableState: TabeStateType = reactive({
loading: false,
size: 'small',
data: [],
});
/**表格字段列 */
let tableColumns: ColumnsType = [
{
title: t('views.logManage.neFile.fileMode'),
dataIndex: 'fileMode',
align: 'center',
width: 150,
},
{
title: t('views.logManage.neFile.owner'),
dataIndex: 'owner',
align: 'left',
width: 100,
},
{
title: t('views.logManage.neFile.group'),
dataIndex: 'group',
align: 'left',
width: 100,
},
{
title: t('views.logManage.neFile.size'),
dataIndex: 'size',
align: 'left',
customRender(opt) {
return parseSizeFromFile(opt.value);
},
width: 100,
},
{
title: t('views.logManage.neFile.modifiedTime'),
dataIndex: 'modifiedTime',
align: 'left',
customRender(opt) {
if (!opt.value) return '';
return parseDateToStr(opt.value);
},
width: 200,
},
{
title: t('views.logManage.neFile.fileName'),
dataIndex: 'fileName',
align: 'left',
},
{
title: t('common.operate'),
key: 'fileName',
align: 'center',
width: 100,
},
];
/**表格分页器参数 */
let tablePagination = reactive({
/**当前页数 */
current: 1,
/**每页条数 */
pageSize: 20,
/**默认的每页条数 */
defaultPageSize: 20,
/**指定每页可以显示多少条 */
pageSizeOptions: ['10', '20', '50', '100'],
/**只有一页时是否隐藏分页器 */
hideOnSinglePage: false,
/**是否可以快速跳转至某页 */
showQuickJumper: true,
/**是否可以改变 pageSize */
showSizeChanger: true,
/**数据总数 */
total: 0,
showTotal: (total: number) => t('common.tablePaginationTotal', { total }),
onChange: (page: number, pageSize: number) => {
tablePagination.current = page;
tablePagination.pageSize = pageSize;
queryParams.pageNum = page;
queryParams.pageSize = pageSize;
fnGetList();
},
});
/**下载触发等待 */
let downLoading = ref<boolean>(false);
/**信息文件下载 */
function fnDownloadFile(row: Record<string, any>) {
if (downLoading.value) return;
Modal.confirm({
title: t('common.tipTitle'),
content: t('views.logManage.exportFile.downTip', {
fileName: row.fileName,
}),
onOk() {
downLoading.value = true;
const hide = message.loading(t('common.loading'), 0);
getFile(queryParams.path, row.fileName)
.then(res => {
if (res.code === RESULT_CODE_SUCCESS) {
message.success({
content: t('common.msgSuccess', {
msg: t('common.downloadText'),
}),
duration: 2,
});
saveAs(res.data, `${row.fileName}`);
} else {
message.error({
content: t('views.logManage.exportFile.downTipErr'),
duration: 2,
});
}
})
.finally(() => {
hide();
downLoading.value = false;
});
},
});
}
/**删除触发等待 */
let delLoading = ref<boolean>(false);
/**信息文件删除 */
function fnRecordDelete(row: Record<string, any>) {
if (delLoading.value) return;
Modal.confirm({
title: t('common.tipTitle'),
content: t('views.logManage.exportFile.deleteTip', {
fileName: row.fileName,
}),
onOk() {
delLoading.value = true;
const hide = message.loading(t('common.loading'), 0);
delFile(queryParams.path, row.fileName)
.then(res => {
if (res.code === RESULT_CODE_SUCCESS) {
message.success({
content: t('common.msgSuccess', {
msg: t('common.deleteText'),
}),
duration: 2,
});
fnGetList();
} else {
message.error({
content: t('views.logManage.exportFile.deleteTipErr'),
duration: 2,
});
}
})
.finally(() => {
hide();
delLoading.value = false;
});
},
});
}
/**网元类型选择对应修改 */
function fnNeChange(_: any, opt: any) {
queryParams.path = `${opt.path}${opt.value}`;
ftpInfo.path = queryParams.path;
ftpInfo.tag = opt.value;
fnGetList(1);
}
/**查询备份信息列表, pageNum初始页数 */
function fnGetList(pageNum?: number) {
if (queryParams.path === '') {
message.warning({
content: t('views.logManage.exportFile.fileSourcePlease'),
duration: 2,
});
return;
}
if (tableState.loading) return;
tableState.loading = true;
if (pageNum) {
queryParams.pageNum = pageNum;
}
listFile(toRaw(queryParams)).then(res => {
if (res.code === RESULT_CODE_SUCCESS) {
const { total, rows } = res.data;
tablePagination.total = total;
tableState.data = rows;
if (
tablePagination.total <=
(queryParams.pageNum - 1) * tablePagination.pageSize &&
queryParams.pageNum !== 1
) {
tableState.loading = false;
fnGetList(queryParams.pageNum - 1);
}
} else {
message.error(res.msg, 3);
tablePagination.total = 0;
tableState.data = [];
}
tableState.loading = false;
});
}
/**打开FTP配置窗口 */
const openFTPModal = ref<boolean>(false);
function fnFTPModalOpen() {
openFTPModal.value = !openFTPModal.value;
}
type FTPInfoType = {
path: string;
tag: string;
fileName: string;
};
const ftpInfo = reactive<FTPInfoType>({
path: '',
tag: '',
fileName: '',
});
/**同步文件到FTP */
function fnSyncFileToFTP(fileName: string) {
ftpInfo.fileName = fileName;
pushBackupFTP(toRaw(ftpInfo)).then(res => {
if (res.code === RESULT_CODE_SUCCESS) {
message.success(t('common.operateOk'), 3);
} else {
message.warning(res.msg, 3);
}
});
}
</script>
<template>
<PageContainer>
<a-card :bordered="false" :body-style="{ padding: '0px' }">
<!-- 插槽-卡片左侧侧 -->
<template #title>
<a-form :model="queryParams" name="queryParams" layout="horizontal">
<a-row :gutter="16" align="middle">
<a-col>
<span>{{ t('views.logManage.exportFile.fileSource') }}:</span
>&nbsp;
<a-select
v-model:value="sourceState.value"
:options="sourceState.list"
@change="fnNeChange"
:allow-clear="false"
:placeholder="t('common.selectPlease')"
style="width: 200px"
/>
</a-col>
<template v-if="queryParams.path">
<span>{{ t('views.logManage.neFile.nePath') }}:</span>&nbsp;
<a-col>
<a-breadcrumb>
<a-breadcrumb-item>
{{ queryParams.path }}
</a-breadcrumb-item>
</a-breadcrumb>
</a-col>
</template>
</a-row>
</a-form>
</template>
<!-- 插槽-卡片右侧 -->
<template #extra>
<a-space :size="8" align="center">
<a-tooltip placement="topRight">
<template #title>
{{ t('views.ne.neConfigBackup.backupModal.title') }}
</template>
<a-button type="text" @click.prevent="fnFTPModalOpen()">
<template #icon><DeliveredProcedureOutlined /></template>
</a-button>
</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-space>
</template>
<!-- 表格列表 -->
<a-table
class="table"
row-key="fileName"
:columns="tableColumns"
:loading="tableState.loading"
:data-source="tableState.data"
:size="tableState.size"
:pagination="tablePagination"
:scroll="{ x: 800 }"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'fileName'">
<a-space :size="8" align="center">
<a-tooltip placement="topRight" v-if="record.fileType === 'file'">
<template #title>
{{ t('views.ne.neConfigBackup.backupModal.pushFileOper') }}
</template>
<a-button
type="link"
@click.prevent="fnSyncFileToFTP(record.fileName)"
>
<template #icon><CloudServerOutlined /></template>
</a-button>
</a-tooltip>
<a-tooltip placement="topRight" v-if="record.fileType === 'file'">
<template #title>{{ t('common.downloadText') }}</template>
<a-button
type="link"
:loading="downLoading"
@click.prevent="fnDownloadFile(record)"
>
<template #icon><DownloadOutlined /></template>
</a-button>
</a-tooltip>
<a-tooltip placement="topRight" v-if="record.fileType === 'file'">
<template #title>{{ t('common.deleteText') }}</template>
<a-button
type="link"
:loading="delLoading"
@click.prevent="fnRecordDelete(record)"
>
<template #icon><DeleteOutlined /></template>
</a-button>
</a-tooltip>
</a-space>
</template>
</template>
</a-table>
</a-card>
<!-- FTP配置窗口 -->
<BackupModal v-model:open="openFTPModal"></BackupModal>
</PageContainer>
</template>
<style lang="less" scoped></style>

View File

@@ -910,6 +910,7 @@ onMounted(() => {
<!-- 状态历史框 --> <!-- 状态历史框 -->
<HistoryModal <HistoryModal
v-if="neTypeAndId.length > 1"
v-model:open="modalState.openByHistory" v-model:open="modalState.openByHistory"
:title="t('views.neData.baseStation.history')" :title="t('views.neData.baseStation.history')"
:ne-type="neTypeAndId[0]" :ne-type="neTypeAndId[0]"

View File

@@ -545,9 +545,13 @@ function fnExportList(type: string) {
} }
/**查询列表, pageNum初始页数 */ /**查询列表, pageNum初始页数 */
function fnGetList() { function fnGetList(pageNum?: number) {
if (tableState.loading) return; if (tableState.loading) return;
tableState.loading = true; tableState.loading = true;
if (pageNum) {
queryParams.pageNum = pageNum;
tablePagination.current = pageNum;
}
listRules(toRaw(queryParams)).then(res => { listRules(toRaw(queryParams)).then(res => {
if (res.code === RESULT_CODE_SUCCESS) { if (res.code === RESULT_CODE_SUCCESS) {
// //
@@ -705,6 +709,7 @@ onMounted(() => {
v-model:value="queryParams.neId" v-model:value="queryParams.neId"
:options="neOtions" :options="neOtions"
:placeholder="t('common.selectPlease')" :placeholder="t('common.selectPlease')"
@change="fnGetList(1)"
/> />
</a-form-item> </a-form-item>
</a-col> </a-col>
@@ -831,7 +836,7 @@ onMounted(() => {
/> />
</a-tooltip> </a-tooltip>
<TableColumnsDnd <TableColumnsDnd
cache-id="pcfData" cache-id="pcfSubData"
:columns="tableColumns" :columns="tableColumns"
v-model:columns-dnd="tableColumnsDnd" v-model:columns-dnd="tableColumnsDnd"
></TableColumnsDnd> ></TableColumnsDnd>
@@ -1190,7 +1195,6 @@ onMounted(() => {
</a-button> </a-button>
</a-col> </a-col>
</a-row> </a-row>
<a-textarea <a-textarea
:disabled="true" :disabled="true"
:hidden="!uploadImportState.msg" :hidden="!uploadImportState.msg"

View File

@@ -780,11 +780,12 @@ onMounted(() => {
<a-form :model="queryParams" name="queryParams" layout="horizontal"> <a-form :model="queryParams" name="queryParams" layout="horizontal">
<a-row :gutter="16"> <a-row :gutter="16">
<a-col :lg="6" :md="12" :xs="24"> <a-col :lg="6" :md="12" :xs="24">
<a-form-item :label="t('views.neUser.auth.neType')" name="neId "> <a-form-item label="UDM" name="neId ">
<a-select <a-select
v-model:value="queryParams.neId" v-model:value="queryParams.neId"
:options="neOtions" :options="neOtions"
:placeholder="t('common.selectPlease')" :placeholder="t('common.selectPlease')"
:disabled="modalState.loadDataLoading"
@change="fnGetList(1)" @change="fnGetList(1)"
/> />
</a-form-item> </a-form-item>
@@ -1268,7 +1269,6 @@ onMounted(() => {
</a-button> </a-button>
</a-col> </a-col>
</a-row> </a-row>
<a-input-password <a-input-password
v-if="uploadImportState.from.typeVal === 'k4'" v-if="uploadImportState.from.typeVal === 'k4'"
v-model:value="uploadImportState.from.typeData" v-model:value="uploadImportState.from.typeData"

View File

@@ -1282,11 +1282,12 @@ onMounted(() => {
<a-form :model="queryParams" name="queryParams" layout="horizontal"> <a-form :model="queryParams" name="queryParams" layout="horizontal">
<a-row :gutter="16"> <a-row :gutter="16">
<a-col :lg="6" :md="12" :xs="24"> <a-col :lg="6" :md="12" :xs="24">
<a-form-item :label="t('views.neUser.sub.neType')" name="neId "> <a-form-item label="UDM" name="neId ">
<a-select <a-select
v-model:value="queryParams.neId" v-model:value="queryParams.neId"
:options="neOtions" :options="neOtions"
:placeholder="t('common.selectPlease')" :placeholder="t('common.selectPlease')"
:disabled="modalState.loadDataLoading"
@change="fnGetList(1)" @change="fnGetList(1)"
/> />
</a-form-item> </a-form-item>
@@ -2284,7 +2285,6 @@ onMounted(() => {
</a-button> </a-button>
</a-col> </a-col>
</a-row> </a-row>
<a-alert <a-alert
:message="uploadImportState.msg" :message="uploadImportState.msg"
:type="uploadImportState.hasFail ? 'warning' : 'info'" :type="uploadImportState.hasFail ? 'warning' : 'info'"

View File

@@ -0,0 +1,944 @@
<script setup lang="ts">
import { reactive, ref, onMounted, toRaw } from 'vue';
import { PageContainer } from 'antdv-pro-layout';
import { ProModal } from 'antdv-pro-modal';
import {
message,
Modal,
Form,
TableColumnsType,
notification,
} from 'ant-design-vue';
import { SizeType } from 'ant-design-vue/es/config-provider';
import { MenuInfo } from 'ant-design-vue/es/menu/src/interface';
import UploadModal from '@/components/UploadModal/index.vue';
import {
addUDMVOIP,
batchAddUDMVOIP,
batchDelUDMVOIP,
delUDMVOIP,
exportUDMVOIP,
importUDMVOIP,
listUDMVOIP,
resetUDMVOIP,
} from '@/api/neData/udm_voip';
import useNeInfoStore from '@/store/modules/neinfo';
import useI18n from '@/hooks/useI18n';
import { RESULT_CODE_SUCCESS } from '@/constants/result-constants';
import { saveAs } from 'file-saver';
import { uploadFileToNE } from '@/api/tool/file';
const { t } = useI18n();
/**网元参数 */
let neOtions = ref<Record<string, any>[]>([]);
/**查询参数 */
let queryParams = reactive({
/**网元ID */
neId: undefined,
/**用户名 */
username: '',
/**排序字段 */
sortField: 'username',
/**排序方式 */
sortOrder: 'asc',
/**当前页数 */
pageNum: 1,
/**每页条数 */
pageSize: 20,
});
/**查询参数重置 */
function fnQueryReset() {
queryParams = Object.assign(queryParams, {
username: '',
sortField: 'username',
sortOrder: 'asc',
});
fnGetList(1);
}
/**表格状态类型 */
type TabeStateType = {
/**加载等待 */
loading: boolean;
/**紧凑型 */
size: SizeType;
/**搜索栏 */
seached: boolean;
/**记录数据 */
data: object[];
/**勾选记录 */
selectedRowKeys: (string | number)[];
};
/**表格状态 */
let tableState: TabeStateType = reactive({
loading: false,
size: 'small',
seached: true,
data: [],
selectedRowKeys: [],
});
/**表格字段列 */
let tableColumns = ref<TableColumnsType>([
{
title: t('views.neData.udmVOIP.username'),
dataIndex: 'username',
sorter: true,
align: 'left',
resizable: true,
width: 250,
minWidth: 100,
maxWidth: 300,
},
{
title: t('common.operate'),
key: 'id',
align: 'left',
},
]);
/**表格字段列排序 */
let tableColumnsDnd = ref<TableColumnsType>([]);
/**表格分页器参数 */
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();
},
});
/**表格分页、排序、筛选变化时触发操作, 排序方式,取值为 ascend descend */
function fnTableChange(pagination: any, filters: any, sorter: any, extra: any) {
const { field, order } = sorter;
if (order) {
queryParams.sortField = field;
queryParams.sortOrder = order.replace('end', '');
} else {
queryParams.sortOrder = 'asc';
}
fnGetList(1);
}
/**表格紧凑型变更操作 */
function fnTableSize({ key }: MenuInfo) {
tableState.size = key as SizeType;
}
/**表格多选 */
function fnTableSelectedRowKeys(keys: (string | number)[]) {
tableState.selectedRowKeys = keys;
}
/**对话框对象信息状态类型 */
type ModalStateType = {
/**新增框或修改框是否显示 */
openByEdit: boolean;
/**标题 */
title: string;
/**表单数据 */
from: Record<string, any>;
/**是否批量操作 */
isBatch: boolean;
/**操作类型 */
type: 'delete' | 'add';
/**确定按钮 loading */
confirmLoading: boolean;
/**更新加载数据按钮 loading */
loadDataLoading: boolean;
};
/**对话框对象信息状态 */
let modalState: ModalStateType = reactive({
openByEdit: false,
title: 'UDMVOIP',
from: {
num: 1,
username: undefined,
password: undefined,
},
isBatch: false,
type: 'add',
confirmLoading: false,
loadDataLoading: false,
});
/**对话框内表单属性和校验规则 */
const modalStateFrom = Form.useForm(
modalState.from,
reactive({
num: [
{
required: true,
message: t('views.neData.common.batchNum'),
},
],
username: [
{ required: true, message: t('views.neData.udmVOIP.usernamePlease') },
],
password: [
{ required: true, message: t('views.neData.udmVOIP.passwordPlease') },
],
})
);
/**
* 对话框弹出显示为 新增或者修改
* @param noticeId 网元id, 不传为新增
*/
function fnModalVisibleByEdit(row?: Record<string, any>) {
modalState.isBatch = false;
if (!row) {
modalStateFrom.resetFields(); //重置表单
modalState.title = t('views.neData.udmVOIP.addTitle');
modalState.openByEdit = true;
modalState.type = 'add';
}
}
/**
* 对话框弹出确认执行函数
* 进行表达规则校验
*/
function fnModalOk() {
const from = JSON.parse(JSON.stringify(modalState.from));
from.neId = queryParams.neId || '-';
from.username = `${from.username}`;
// 校验规则
let validateArr = ['username', 'password'];
if (modalState.isBatch) {
validateArr.push('num');
if (modalState.type === 'delete') {
validateArr = ['num', 'username'];
}
}
modalStateFrom
.validate(validateArr)
.then(e => {
modalState.confirmLoading = true;
const hide = message.loading(t('common.loading'), 0);
// 根据类型选择函数
let result: any = null;
if (modalState.isBatch) {
if (modalState.type === 'add') {
result = batchAddUDMVOIP(
from.neId,
{ username: from.username, password: from.password },
from.num
);
}
if (modalState.type === 'delete') {
result = batchDelUDMVOIP(from.neId, from.username, from.num);
}
} else {
if (modalState.type === 'add') {
result = addUDMVOIP(from.neId, {
username: from.username,
password: from.password,
});
}
}
result
.then((res: any) => {
if (res.code === RESULT_CODE_SUCCESS) {
message.success({
content: t('common.operateOk'),
duration: 3,
});
fnModalCancel();
fnGetList();
} else {
message.error({
content: `${res.msg}`,
duration: 3,
});
}
})
.finally(() => {
hide();
modalState.confirmLoading = false;
});
})
.catch(e => {
message.error(t('common.errorFields', { num: e.errorFields.length }), 3);
});
}
/**
* 对话框弹出关闭执行函数
* 进行表达规则校验
*/
function fnModalCancel() {
modalState.type = 'add';
modalState.isBatch = false;
modalState.openByEdit = false;
modalStateFrom.resetFields();
}
/**
* 对话框弹出显示为 批量操作
* @param type 类型
*/
function fnModalVisibleByBatch(type: 'delete' | 'add') {
modalStateFrom.resetFields(); //重置表单
modalState.isBatch = true;
modalState.type = type;
if (type === 'add') {
modalState.title = t('views.neData.common.batchAddText');
modalState.openByEdit = true;
}
if (type === 'delete') {
modalState.title = t('views.neData.common.batchDelText');
modalState.openByEdit = true;
}
}
/**
* 记录删除
* @param username 网元编号ID
*/
function fnRecordDelete(username: string) {
const neID = queryParams.neId;
if (!neID) return;
let msg = username;
if (username === '0') {
msg = `${tableState.selectedRowKeys[0]}... ${tableState.selectedRowKeys.length}`;
username = tableState.selectedRowKeys.join(',');
}
Modal.confirm({
title: t('common.tipTitle'),
content: t('views.neData.udmVOIP.delTip', { num: msg }),
onOk() {
const hide = message.loading(t('common.loading'), 0);
delUDMVOIP(neID, username)
.then(res => {
if (res.code === RESULT_CODE_SUCCESS) {
message.success(t('common.operateOk'), 3);
fnGetList();
} else {
message.error({
content: `${res.msg}`,
duration: 3,
});
}
})
.finally(() => {
hide();
});
},
});
}
/**列表导出 */
function fnExportList(type: string) {
const neId = queryParams.neId;
if (!neId) return;
const hide = message.loading(t('common.loading'), 0);
exportUDMVOIP(Object.assign({ type: type }, queryParams))
.then(res => {
if (res.code === RESULT_CODE_SUCCESS) {
message.success({
content: t('common.operateOk'),
duration: 2,
});
saveAs(res.data, `UDM_VOIP_${neId}_${Date.now()}.${type}`);
} else {
message.error({
content: `${res.msg}`,
duration: 2,
});
}
})
.finally(() => {
hide();
});
}
/**查询列表, pageNum初始页数 */
function fnGetList(pageNum?: number) {
if (tableState.loading) return;
tableState.loading = true;
if (pageNum) {
queryParams.pageNum = pageNum;
tablePagination.current = pageNum;
}
listUDMVOIP(toRaw(queryParams)).then(res => {
if (res.code === RESULT_CODE_SUCCESS) {
// 取消勾选
if (tableState.selectedRowKeys.length > 0) {
tableState.selectedRowKeys = [];
}
const { total, rows } = res.data;
tablePagination.total = total;
tableState.data = rows;
} else {
tableState.data = [];
}
tableState.loading = false;
});
}
/**重新加载数据 */
function fnLoadData() {
const neId = queryParams.neId;
if (tableState.loading || !neId) return;
modalState.loadDataLoading = true;
tablePagination.total = 0;
tableState.data = [];
tableState.loading = true; // 表格loading
resetUDMVOIP(neId).then(res => {
if (res.code === RESULT_CODE_SUCCESS) {
const num = res.data;
const timerS = Math.ceil(+num / 800) + 3;
notification.success({
message: t('views.neData.common.loadData'),
description: t('views.neData.common.loadDataTip', {
num,
timer: timerS,
}),
duration: timerS,
});
// 延迟10s后关闭loading刷新列表
setTimeout(() => {
modalState.loadDataLoading = false;
tableState.loading = false; // 表格loading
fnQueryReset();
}, timerS * 1000);
} else {
modalState.loadDataLoading = false;
tableState.loading = false; // 表格loading
fnQueryReset();
message.error({
content: t('common.getInfoFail'),
duration: 3,
});
}
});
}
/**对话框表格信息导入对象信息状态类型 */
type ModalUploadImportStateType = {
/**是否显示 */
open: boolean;
/**标题 */
title: string;
/**是否上传中 */
loading: boolean;
/**上传结果信息 */
msg: string;
/**含失败信息 */
hasFail: boolean;
};
/**对话框表格信息导入对象信息状态 */
let uploadImportState: ModalUploadImportStateType = reactive({
open: false,
title: t('components.UploadModal.uploadTitle'),
loading: false,
msg: '',
hasFail: false,
});
/**对话框表格信息导入弹出窗口 */
function fnModalUploadImportOpen() {
uploadImportState.msg = '';
uploadImportState.hasFail = false;
uploadImportState.loading = false;
uploadImportState.open = true;
}
/**对话框表格信息导入关闭窗口 */
function fnModalUploadImportClose() {
uploadImportState.open = false;
fnGetList();
}
/**对话框表格信息导入上传 */
function fnModalUploadImportUpload(file: File) {
const neID = queryParams.neId;
if (!neID) {
return Promise.reject('Unknown network element');
}
const hide = message.loading(t('common.loading'), 0);
uploadImportState.loading = true;
uploadFileToNE('UDM', neID, file, 5)
.then(res => {
if (res.code === RESULT_CODE_SUCCESS) {
return importUDMVOIP({
neId: neID,
uploadPath: res.data,
});
}
return res;
})
.then(res => {
if (!res) return;
uploadImportState.msg = res.msg;
const regex = /fail num: (\d+)/;
const match = res.msg.match(regex);
if (match) {
const failNum = Number(match[1]);
uploadImportState.hasFail = failNum > 0;
} else {
uploadImportState.hasFail = false;
}
})
.finally(() => {
hide();
uploadImportState.loading = false;
});
}
/**对话框表格信息导入模板 */
function fnModalDownloadImportTemplate() {
const hide = message.loading(t('common.loading'), 0);
const baseUrl = import.meta.env.VITE_HISTORY_BASE_URL;
const templateUrl = `${
baseUrl.length === 1 && baseUrl.indexOf('/') === 0
? ''
: baseUrl.indexOf('/') === -1
? '/' + baseUrl
: baseUrl
}/neDataImput`;
saveAs(
`${templateUrl}/udm_voip_template.txt`,
`import_udmvoip_template_${Date.now()}.txt`
);
hide();
}
onMounted(() => {
// 获取网元网元列表
useNeInfoStore()
.fnNelist()
.then(res => {
if (res.code === RESULT_CODE_SUCCESS && res.data?.length > 0) {
let arr: Record<string, any>[] = [];
res.data.forEach((v: any) => {
if (v.neType === 'UDM') {
arr.push({ value: v.neId, label: v.neName });
}
});
neOtions.value = arr;
if (arr.length > 0) {
queryParams.neId = arr[0].value;
}
} else {
message.warning({
content: t('common.noData'),
duration: 2,
});
}
})
.finally(() => {
// 获取列表数据
fnGetList();
});
});
</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="UDM" name="neId ">
<a-select
v-model:value="queryParams.neId"
:options="neOtions"
:placeholder="t('common.selectPlease')"
:disabled="modalState.loadDataLoading"
@change="fnGetList(1)"
/>
</a-form-item>
</a-col>
<a-col :lg="6" :md="12" :xs="24">
<a-form-item
:label="t('views.neData.udmVOIP.username')"
name="username"
>
<a-input
v-model:value="queryParams.username"
allow-clear
:placeholder="t('common.inputPlease')"
></a-input>
</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()">
<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>
<PlusOutlined />
</template>
{{ t('common.addText') }}
</a-button>
<a-button
type="default"
danger
:disabled="tableState.selectedRowKeys.length <= 0"
:loading="modalState.confirmLoading"
@click.prevent="fnRecordDelete('0')"
>
<template #icon><DeleteOutlined /></template>
{{ t('views.neData.common.checkDel') }}
</a-button>
<a-dropdown trigger="click">
<a-button>
{{ t('views.neData.common.batchOper') }}
<DownOutlined />
</a-button>
<template #overlay>
<a-menu @click="({ key }:any) => fnModalVisibleByBatch(key)">
<a-menu-item key="add">
<PlusOutlined />
{{ t('views.neData.common.batchAddText') }}
</a-menu-item>
<a-menu-item key="delete">
<DeleteOutlined />
{{ t('views.neData.common.batchDelText') }}
</a-menu-item>
</a-menu>
</template>
</a-dropdown>
<a-popconfirm
placement="topRight"
:title="t('views.neData.common.loadDataConfirm')"
:ok-text="t('common.ok')"
:cancel-text="t('common.cancel')"
:disabled="modalState.loadDataLoading"
@confirm="fnLoadData"
>
<a-button
type="dashed"
danger
:disabled="modalState.loadDataLoading"
:loading="modalState.loadDataLoading"
>
<template #icon><SyncOutlined /></template>
{{ t('views.neData.common.loadData') }}
</a-button>
</a-popconfirm>
<a-button type="dashed" @click.prevent="fnModalUploadImportOpen">
<template #icon><ImportOutlined /></template>
{{ t('common.import') }}
</a-button>
<a-popconfirm
placement="topRight"
:title="t('views.neData.udmVOIP.exportTip')"
ok-text="TXT"
ok-type="default"
@confirm="fnExportList('txt')"
>
<a-button type="dashed">
<template #icon><ExportOutlined /></template>
{{ t('common.export') }}
</a-button>
</a-popconfirm>
</a-space>
</template>
<!-- 插槽-卡片右侧 -->
<template #extra>
<a-space :size="8" align="center">
<a-tooltip placement="topRight">
<template #title>
{{
tableState.seached
? t('common.switch.show')
: t('common.switch.hide')
}}
</template>
<a-switch
v-model:checked="tableState.seached"
:checked-children="t('common.searchBarText')"
:un-checked-children="t('common.searchBarText')"
size="small"
/>
</a-tooltip>
<TableColumnsDnd
cache-id="udmVOIPData"
:columns="tableColumns"
v-model:columns-dnd="tableColumnsDnd"
></TableColumnsDnd>
<a-tooltip placement="topRight">
<template #title>{{ t('common.reloadText') }}</template>
<a-button type="text" @click.prevent="fnGetList()">
<template #icon><ReloadOutlined /></template>
</a-button>
</a-tooltip>
<a-tooltip placement="topRight">
<template #title>{{ t('common.sizeText') }}</template>
<a-dropdown placement="bottomRight" trigger="click">
<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="username"
:columns="tableColumnsDnd"
:loading="tableState.loading"
:data-source="tableState.data"
:size="tableState.size"
:pagination="tablePagination"
:scroll="{ y: 'calc(100vh - 480px)' }"
@change="fnTableChange"
@resizeColumn="(w:number, col:any) => (col.width = w)"
:row-selection="{
type: 'checkbox',
selectedRowKeys: tableState.selectedRowKeys,
onChange: fnTableSelectedRowKeys,
}"
>
<template #bodyCell="{ column, record }">
<template v-if="column?.key === 'id'">
<a-space :size="8" align="center">
<a-tooltip>
<template #title>{{ t('common.deleteText') }}</template>
<a-button
type="link"
@click.prevent="fnRecordDelete(record.username)"
>
<template #icon>
<DeleteOutlined />
</template>
</a-button>
</a-tooltip>
</a-space>
</template>
</template>
</a-table>
</a-card>
<!-- 新增框或修改框 -->
<ProModal
:drag="true"
:width="520"
:destroyOnClose="true"
:keyboard="false"
:mask-closable="false"
:open="modalState.openByEdit"
:title="modalState.title"
:confirm-loading="modalState.confirmLoading"
@ok="fnModalOk"
@cancel="fnModalCancel"
>
<a-form
name="modalStateFrom"
layout="horizontal"
:label-col="{ span: 6 }"
:labelWrap="true"
>
<!--批量删除-->
<template v-if="modalState.isBatch && modalState.type === 'delete'">
<a-form-item
:label="t('views.neData.common.batchNum')"
name="num"
v-bind="modalStateFrom.validateInfos.num"
>
<a-input-number
v-model:value="modalState.from.num"
style="width: 100%"
:min="1"
:max="500"
placeholder="<=500"
></a-input-number>
</a-form-item>
<a-form-item
:label="
modalState.isBatch
? t('views.neData.udmVOIP.startUsername')
: t('views.neData.udmVOIP.username')
"
name="username"
v-bind="modalStateFrom.validateInfos.username"
>
<a-input-number
v-model:value="modalState.from.username"
style="width: 100%"
:min="4"
:maxlangth="16"
:placeholder="t('views.neData.udmVOIP.username')"
>
</a-input-number>
</a-form-item>
</template>
<template v-else>
<!--批量数-->
<a-form-item
v-if="modalState.isBatch"
:label="t('views.neData.common.batchNum')"
name="num"
v-bind="modalStateFrom.validateInfos.num"
>
<a-input-number
v-model:value="modalState.from.num"
style="width: 100%"
:min="1"
:max="500"
placeholder="<=500"
></a-input-number>
</a-form-item>
<a-form-item
:label="
modalState.isBatch
? t('views.neData.udmVOIP.startUsername')
: t('views.neData.udmVOIP.username')
"
name="username"
v-bind="modalStateFrom.validateInfos.username"
>
<a-input-number
v-model:value="modalState.from.username"
style="width: 100%"
:min="4"
:maxlength="16"
:placeholder="t('views.neData.udmVOIP.username')"
>
</a-input-number>
</a-form-item>
<a-form-item
:label="t('views.neData.udmVOIP.password')"
name="password"
v-bind="modalStateFrom.validateInfos.password"
>
<a-input-password
v-model:value="modalState.from.password"
style="width: 100%"
:min="4"
:max="64"
:placeholder="t('views.neData.udmVOIP.password')"
>
</a-input-password>
</a-form-item>
</template>
</a-form>
</ProModal>
<!-- 上传导入表格数据文件框 -->
<UploadModal
:title="uploadImportState.title"
:loading="uploadImportState.loading"
@upload="fnModalUploadImportUpload"
@close="fnModalUploadImportClose"
v-model:open="uploadImportState.open"
:ext="['.txt']"
>
<template #default>
<a-row justify="space-between" align="middle">
<a-col :span="12"> </a-col>
<a-col>
<a-button
type="link"
:title="t('views.neData.common.importTemplate')"
@click.prevent="fnModalDownloadImportTemplate"
>
{{ t('views.neData.common.importTemplate') }}
</a-button>
</a-col>
</a-row>
<a-textarea
:disabled="true"
:hidden="!uploadImportState.msg"
:value="uploadImportState.msg"
:auto-size="{ minRows: 2, maxRows: 8 }"
style="background-color: transparent; color: rgba(0, 0, 0, 0.85)"
/>
</template>
</UploadModal>
</PageContainer>
</template>
<style lang="less" scoped>
.table :deep(.ant-pagination) {
padding: 0 24px;
}
</style>

File diff suppressed because it is too large Load Diff

View File

@@ -2,7 +2,7 @@
import { GlobalFooter } from 'antdv-pro-layout'; import { GlobalFooter } from 'antdv-pro-layout';
import { Modal, message } from 'ant-design-vue/es'; import { Modal, message } from 'ant-design-vue/es';
import { computed, onMounted, reactive, toRaw } from 'vue'; import { computed, onMounted, reactive, toRaw } from 'vue';
import { register } from '@/api/login'; import { register } from '@/api/auth';
import { regExpPasswd, regExpUserName } from '@/utils/regular-utils'; import { regExpPasswd, regExpUserName } from '@/utils/regular-utils';
import { useRouter, useRoute } from 'vue-router'; import { useRouter, useRoute } from 'vue-router';
import useAppStore from '@/store/modules/app'; import useAppStore from '@/store/modules/app';

View File

@@ -272,7 +272,7 @@ function fnUnlock() {
hide(); hide();
if (res.code === RESULT_CODE_SUCCESS) { if (res.code === RESULT_CODE_SUCCESS) {
message.success({ message.success({
content: t('views.system.log.login.unlockSure', { content: t('views.system.log.login.unlockSuss', {
userName: username, userName: username,
}), }),
duration: 3, duration: 3,

View File

@@ -2,9 +2,8 @@
import { stepState, fnToStepName } from '../hooks/useStep'; import { stepState, fnToStepName } from '../hooks/useStep';
import { Modal } from 'ant-design-vue/es'; import { Modal } from 'ant-design-vue/es';
import { RESULT_CODE_SUCCESS } from '@/constants/result-constants'; import { RESULT_CODE_SUCCESS } from '@/constants/result-constants';
import { removeToken } from '@/plugins/auth-token'; import { delAccessToken, delRefreshToken } from '@/plugins/auth-token';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import useAppStore from '@/store/modules/app';
import useI18n from '@/hooks/useI18n'; import useI18n from '@/hooks/useI18n';
import { onMounted, ref } from 'vue'; import { onMounted, ref } from 'vue';
import { bootloaderDone } from '@/api/system/quick-start/bootloader'; import { bootloaderDone } from '@/api/system/quick-start/bootloader';
@@ -56,8 +55,9 @@ function fnGuideDone() {
bootloaderDone() bootloaderDone()
.then(res => { .then(res => {
if (res.code === RESULT_CODE_SUCCESS) { if (res.code === RESULT_CODE_SUCCESS) {
removeToken();
// useAppStore().bootloader = false; // useAppStore().bootloader = false;
delAccessToken();
delRefreshToken();
} }
}) })
.finally(() => { .finally(() => {

View File

@@ -2,8 +2,7 @@
import { onMounted } from 'vue'; import { onMounted } from 'vue';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import { RESULT_CODE_SUCCESS } from '@/constants/result-constants'; import { RESULT_CODE_SUCCESS } from '@/constants/result-constants';
import { TOKEN_RESPONSE_FIELD } from '@/constants/token-constants'; import { getAccessToken, setAccessToken } from '@/plugins/auth-token';
import { getToken, setToken } from '@/plugins/auth-token';
import { bootloaderStart } from '@/api/system/quick-start/bootloader'; import { bootloaderStart } from '@/api/system/quick-start/bootloader';
import { fnToStepName } from '../hooks/useStep'; import { fnToStepName } from '../hooks/useStep';
import useI18n from '@/hooks/useI18n'; import useI18n from '@/hooks/useI18n';
@@ -19,11 +18,10 @@ function fnChangeLocale(e: any) {
/**引导开始 */ /**引导开始 */
function fnGuideStart() { function fnGuideStart() {
if (getToken()) return; if (getAccessToken()) return;
bootloaderStart().then(res => { bootloaderStart().then(res => {
if (res.code === RESULT_CODE_SUCCESS && res.data) { if (res.code === RESULT_CODE_SUCCESS && res.data) {
const token = res.data[TOKEN_RESPONSE_FIELD]; setAccessToken(res.data.accessToken, res.data.expiresIn);
setToken(token);
} else { } else {
router.push({ name: 'Login' }); router.push({ name: 'Login' });
} }

View File

@@ -32,8 +32,10 @@ import { hasPermissions } from '@/plugins/auth-user';
import { MENU_PATH_INLINE } from '@/constants/menu-constants'; import { MENU_PATH_INLINE } from '@/constants/menu-constants';
import { RESULT_CODE_SUCCESS } from '@/constants/result-constants'; import { RESULT_CODE_SUCCESS } from '@/constants/result-constants';
import useI18n from '@/hooks/useI18n'; import useI18n from '@/hooks/useI18n';
import useUserStore from '@/store/modules/user';
const { t } = useI18n(); const { t } = useI18n();
const { getDict } = useDictStore(); const { getDict } = useDictStore();
const userStore = useUserStore();
const router = useRouter(); const router = useRouter();
const route = useRoute(); const route = useRoute();
const routePath = route.path; const routePath = route.path;
@@ -732,7 +734,7 @@ function fnGetList(pageNum?: number) {
queryParams.beginTime = queryRangePicker.value[0]; queryParams.beginTime = queryRangePicker.value[0];
queryParams.endTime = queryRangePicker.value[1]; queryParams.endTime = queryRangePicker.value[1];
listRole(toRaw(queryParams)).then(res => { listRole(toRaw(queryParams)).then(res => {
if (res.code === RESULT_CODE_SUCCESS) { if (res.code === RESULT_CODE_SUCCESS) {
// 取消勾选 // 取消勾选
if (tableState.selectedRowKeys.length > 0) { if (tableState.selectedRowKeys.length > 0) {
tableState.selectedRowKeys = []; tableState.selectedRowKeys = [];
@@ -941,6 +943,7 @@ onMounted(() => {
v-if=" v-if="
dict.sysNormalDisable.length > 0 && dict.sysNormalDisable.length > 0 &&
record.roleId !== 1 && record.roleId !== 1 &&
!userStore.roles?.includes(record.roleKey) &&
hasPermissions(['system:role:edit']) hasPermissions(['system:role:edit'])
" "
v-model:checked="record.statusFlag" v-model:checked="record.statusFlag"
@@ -969,7 +972,12 @@ onMounted(() => {
<template #icon><ProfileOutlined /></template> <template #icon><ProfileOutlined /></template>
</a-button> </a-button>
</a-tooltip> </a-tooltip>
<a-tooltip v-if="record.roleId !== 1"> <a-tooltip
v-if="
record.roleId !== 1 &&
!userStore.roles?.includes(record.roleKey)
"
>
<template #title>{{ t('common.editText') }}</template> <template #title>{{ t('common.editText') }}</template>
<a-button <a-button
type="link" type="link"
@@ -979,7 +987,12 @@ onMounted(() => {
<template #icon><FormOutlined /></template> <template #icon><FormOutlined /></template>
</a-button> </a-button>
</a-tooltip> </a-tooltip>
<a-tooltip v-if="record.roleId !== 1"> <a-tooltip
v-if="
record.roleId !== 1 &&
!userStore.roles?.includes(record.roleKey)
"
>
<template #title>{{ t('common.deleteText') }}</template> <template #title>{{ t('common.deleteText') }}</template>
<a-button <a-button
type="link" type="link"
@@ -1001,7 +1014,7 @@ onMounted(() => {
<template #icon><SecurityScanOutlined /></template> <template #icon><SecurityScanOutlined /></template>
</a-button> </a-button>
</a-tooltip> </a-tooltip>
<a-tooltip placement="topRight" v-if="record.roleId !== 1"> <a-tooltip placement="topRight" v-if="false">
<template #title>{{ <template #title>{{
t('views.system.role.distributeUser') t('views.system.role.distributeUser')
}}</template> }}</template>

View File

@@ -29,7 +29,6 @@ const password = ref('');
/**解锁 */ /**解锁 */
function handleUnlock() { function handleUnlock() {
if (maskStore.lockPasswd === password.value) { if (maskStore.lockPasswd === password.value) {
message.success(t('components.LockScreen.validSucc'), 3);
password.value = ''; password.value = '';
maskStore.handleMaskType('none'); maskStore.handleMaskType('none');
const redirectPath = route.query?.redirect || '/index'; const redirectPath = route.query?.redirect || '/index';
@@ -79,7 +78,7 @@ onMounted(() => {
</span> </span>
</div> </div>
<div class="lock-screen_login-from"> <div class="lock-screen_login-from">
<a-input-group compact> <a-input-group compact v-if="maskStore.lockPasswd">
<a-input <a-input
type="password" type="password"
v-model:value="password" v-model:value="password"
@@ -92,6 +91,10 @@ onMounted(() => {
<LoginOutlined /> <LoginOutlined />
</a-button> </a-button>
</a-input-group> </a-input-group>
<a-button type="primary" block @click="handleUnlock" v-else>
{{ t('components.LockScreen.enter') }}
</a-button>
<a-button type="text" class="logout" @click="handleBackLogin"> <a-button type="text" class="logout" @click="handleBackLogin">
{{ t('components.LockScreen.backLogin') }} {{ t('components.LockScreen.backLogin') }}
</a-button> </a-button>

View File

@@ -7,7 +7,7 @@ import { Modal, message } from 'ant-design-vue/es';
import { parseDateToStr } from '@/utils/date-utils'; import { parseDateToStr } from '@/utils/date-utils';
import { getNeDirZip, getNeFile, listNeFiles } from '@/api/tool/neFile'; import { getNeDirZip, getNeFile, listNeFiles } from '@/api/tool/neFile';
import { RESULT_CODE_SUCCESS } from '@/constants/result-constants'; import { RESULT_CODE_SUCCESS } from '@/constants/result-constants';
import ViewDrawer from '@/views/logManage/neFile/components/ViewDrawer.vue'; import ViewDrawer from '@/views/ne/neFile/components/ViewDrawer.vue';
import useNeInfoStore from '@/store/modules/neinfo'; import useNeInfoStore from '@/store/modules/neinfo';
import useTabsStore from '@/store/modules/tabs'; import useTabsStore from '@/store/modules/tabs';
import useI18n from '@/hooks/useI18n'; import useI18n from '@/hooks/useI18n';

View File

@@ -1,35 +1,32 @@
<script setup lang="ts"> <script setup lang="ts">
import { onBeforeUnmount, ref, watch } from 'vue'; import { onBeforeUnmount, onMounted, reactive, ref, toRaw } from 'vue';
import { useRoute, useRouter } from 'vue-router'; import { useRoute, useRouter } from 'vue-router';
import { PageContainer } from 'antdv-pro-layout'; import { PageContainer } from 'antdv-pro-layout';
import { message, Modal } from 'ant-design-vue/es'; import { message, Modal } from 'ant-design-vue';
import { SizeType } from 'ant-design-vue/es/config-provider';
import { ColumnsType } from 'ant-design-vue/es/table';
import DissectionTree from '../tshark/components/DissectionTree.vue'; import DissectionTree from '../tshark/components/DissectionTree.vue';
import DissectionDump from '../tshark/components/DissectionDump.vue'; import DissectionDump from '../tshark/components/DissectionDump.vue';
import PacketTable from '../tshark/components/PacketTable.vue';
import { usePCAP, NO_SELECTION } from '../tshark/hooks/usePCAP';
import { import {
RESULT_CODE_ERROR, RESULT_CODE_ERROR,
RESULT_CODE_SUCCESS, RESULT_CODE_SUCCESS,
} from '@/constants/result-constants'; } from '@/constants/result-constants';
import { filePullTask } from '@/api/trace/task'; import { filePullTask, getTraceData, listTraceData } from '@/api/trace/task';
import { OptionsType, WS } from '@/plugins/ws-websocket'; import { scriptUrl as wkUrl } from '@/assets/js/wiregasm_worker';
import * as wkUtil from '@/plugins/wk-worker';
import * as wsUtil from '@/plugins/ws-websocket';
import useI18n from '@/hooks/useI18n'; import useI18n from '@/hooks/useI18n';
import useDictStore from '@/store/modules/dict';
import useTabsStore from '@/store/modules/tabs'; import useTabsStore from '@/store/modules/tabs';
import saveAs from 'file-saver'; import saveAs from 'file-saver';
import { parseDateToStr } from '@/utils/date-utils';
const { getDict } = useDictStore();
const { t } = useI18n();
const route = useRoute(); const route = useRoute();
const router = useRouter(); const router = useRouter();
const tabsStore = useTabsStore(); const tabsStore = useTabsStore();
const ws = new WS(); const ws = new wsUtil.WS();
const { t } = useI18n(); const wk = new wkUtil.WK();
const {
state,
handleSelectedTreeEntry,
handleSelectedFindSelection,
handleSelectedFrame,
handleScrollBottom,
handleFilterFrames,
handleLoadFile,
} = usePCAP();
/**跟踪编号 */ /**跟踪编号 */
const traceId = ref<string>(route.query.traceId as string); const traceId = ref<string>(route.query.traceId as string);
@@ -44,6 +41,17 @@ function fnClose() {
} }
} }
/**字典数据 */
let dict: {
/**跟踪消息类型 */
traceMsgType: DictType[];
/**跟踪消息方向 */
traceMsgDirect: DictType[];
} = reactive({
traceMsgType: [],
traceMsgDirect: [],
});
/**下载触发等待 */ /**下载触发等待 */
let downLoading = ref<boolean>(false); let downLoading = ref<boolean>(false);
@@ -82,15 +90,266 @@ function fnDownloadFile() {
}); });
} }
/**获取PCAP文件 */ // =========== 表格数据 ==============
function fnFilePCAP() { /**表格状态类型 */
filePullTask(traceId.value).then(res => { type TabeStateType = {
/**加载等待 */
loading: boolean;
/**紧凑型 */
size: SizeType;
/**记录数据 */
data: object[];
/**点击选择行 */
row: Record<string, any>;
};
/**表格状态 */
let tableState: TabeStateType = reactive({
loading: false,
size: 'small',
data: [],
row: {},
});
/**表格字段列 */
let tableColumns: ColumnsType = [
{
title: t('views.traceManage.task.msgNe'),
dataIndex: 'msgNe',
align: 'left',
width: 150,
},
{
title: t('views.traceManage.task.rowTime'),
dataIndex: 'timestamp',
align: 'left',
width: 250,
customRender(opt) {
if (!opt.value) return '';
const nanoseconds = opt.value; // 纳秒时间戳
const milliseconds = Math.floor(nanoseconds / 1000000);
const nanosecondsPart = (nanoseconds % 1000000)
.toString()
.padStart(6, '0');
return `${parseDateToStr(
milliseconds,
'YYYY-MM-DD HH:mm:ss.SSS'
)}.${nanosecondsPart}`;
},
sorter: true,
},
{
title: t('views.traceManage.task.protocolOrInterface'),
dataIndex: 'ifType',
align: 'left',
width: 150,
},
{
title: t('views.traceManage.task.msgEvent'),
dataIndex: 'msgEvent',
align: 'left',
ellipsis: true,
},
{
title: t('views.traceManage.task.msgType'),
dataIndex: 'msgType',
key: 'msgType',
align: 'left',
width: 100,
},
{
title: t('views.traceManage.task.msgDirect'),
dataIndex: 'msgDirect',
key: 'msgDirect',
align: 'left',
width: 100,
},
{
title: t('views.traceManage.task.srcIp'),
dataIndex: 'srcAddr',
align: 'left',
width: 150,
},
{
title: t('views.traceManage.task.dstIp'),
dataIndex: 'dstAddr',
align: 'left',
width: 150,
},
];
/**表格分页器参数 */
let tablePagination = reactive({
/**当前页数 */
current: 1,
/**每页条数 */
pageSize: 10,
/**默认的每页条数 */
defaultPageSize: 10,
/**指定每页可以显示多少条 */
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();
},
});
/**表格分页、排序、筛选变化时触发操作, 排序方式,取值为 ascend descend */
function fnTableChange(pagination: any, filters: any, sorter: any, extra: any) {
const { field, order } = sorter;
if (order) {
queryParams.sortBy = field;
queryParams.sortOrder = order.replace('end', '');
} else {
queryParams.sortOrder = 'asc';
}
fnGetList(1);
}
/**查询参数 */
let queryParams = reactive({
traceId: traceId.value,
sortBy: 'timestamp',
sortOrder: 'asc',
/**当前页数 */
pageNum: 1,
/**每页条数 */
pageSize: 10,
});
/**查询备份信息列表, pageNum初始页数 */
function fnGetList(pageNum?: number) {
if (tableState.loading) return;
tableState.loading = true;
if (pageNum) {
queryParams.pageNum = pageNum;
}
listTraceData(toRaw(queryParams)).then(res => {
if (res.code === RESULT_CODE_SUCCESS) { if (res.code === RESULT_CODE_SUCCESS) {
handleLoadFile(res.data); const { total, rows } = res.data;
tablePagination.total = total;
tableState.data = rows;
if (
tablePagination.total <=
(queryParams.pageNum - 1) * tablePagination.pageSize &&
queryParams.pageNum !== 1
) {
tableState.loading = false;
fnGetList(queryParams.pageNum - 1);
}
} }
tableState.loading = false;
}); });
} }
/**
* 查看帧数据
* @param row 记录信息
*/
function fnVisible(row: Record<string, any>) {
// 选中行重复点击时显示隐藏
if (row.id === tableState.row.id && state.selectedFrame !== 0) {
state.selectedFrame = 0;
return;
}
getTraceData(row.id).then(res => {
if (res.code === RESULT_CODE_SUCCESS) {
Object.assign(tableState.row, res.data);
const blob = generatePCAP(res.data.timestamp / 1e3, res.data.rawMsg);
wk.send({ type: 'process', file: blob });
}
});
}
/**生成PCAP-blob */
function generatePCAP(timestamp: number, base64Data: string): Blob {
// 1. 转换数据
const binaryString = atob(base64Data);
const len = binaryString.length;
const bytes = new Uint8Array(len);
for (let i = 0; i < len; i++) {
bytes[i] = binaryString.charCodeAt(i);
}
// 2. 创建PCAP文件头
const fileHeader = new Uint8Array([
0xd4,
0xc3,
0xb2,
0xa1, // magic_number (微秒级)
0x02,
0x00,
0x04,
0x00, // version_major(2) + version_minor(4)
0x00,
0x00,
0x00,
0x00, // thiszone (UTC)
0x00,
0x00,
0x00,
0x00, // sigfigs (固定0)
0x00,
0x00,
0x04,
0x00, // snaplen (1024)
0x71,
0x00,
0x00,
0x00, // network (LINKTYPE_LINUX_SLL)
]);
// 3. 构造Linux cooked头
const cookedHeader = new Uint8Array(16);
const view = new DataView(cookedHeader.buffer);
view.setUint16(0, 0x0000, false); // 数据包类型(主机→网络)
view.setUint16(2, 0x0304, false); // 地址类型ARPHRD_ETHER
view.setUint16(4, 0x0008, false); // 协议类型ETH_P_IP
view.setUint16(14, 0x0800, false); // 数据包类型PACKET_HOST
// 4. 合并链路层头与数据
const fullData = new Uint8Array(cookedHeader.length + bytes.length);
fullData.set(cookedHeader);
fullData.set(bytes, cookedHeader.length);
// 5. 生成时间戳
const date = new Date(timestamp);
const secs = Math.floor(date.getTime() / 1000);
const usecs = (date.getTime() % 1000) * 1000;
// 6. 构造PCAP报文头
const packetHeader = new Uint8Array(16);
const headerView = new DataView(packetHeader.buffer);
headerView.setUint32(0, secs, true); // 时间戳秒
headerView.setUint32(4, usecs, true); // 时间戳微秒
headerView.setUint32(8, fullData.length, true); // 捕获长度
headerView.setUint32(12, fullData.length, true); // 原始长度
// 7. 合并所有数据
const finalData = new Uint8Array(
fileHeader.length + packetHeader.length + fullData.length
);
finalData.set(fileHeader);
finalData.set(packetHeader, fileHeader.length);
finalData.set(fullData, fileHeader.length + packetHeader.length);
// 8. 文件Blob对象
return new Blob([finalData], { type: 'application/octet-stream' });
}
// =========== WS ==============
/**接收数据后回调 */ /**接收数据后回调 */
function wsMessage(res: Record<string, any>) { function wsMessage(res: Record<string, any>) {
const { code, requestId, data } = res; const { code, requestId, data } = res;
@@ -101,7 +360,6 @@ function wsMessage(res: Record<string, any>) {
// 建联时发送请求 // 建联时发送请求
if (!requestId && data.clientId) { if (!requestId && data.clientId) {
fnFilePCAP();
return; return;
} }
@@ -110,13 +368,16 @@ function wsMessage(res: Record<string, any>) {
return; return;
} }
if (data.groupId === `2_${traceId.value}`) { if (data.groupId === `2_${traceId.value}`) {
fnFilePCAP(); // 第一页降序时实时添加记录
if (queryParams.pageNum === 1 && queryParams.sortOrder === 'desc') {
tableState.data.unshift(data);
}
tablePagination.total += 1;
} }
} }
/**建立WS连接 */ /**建立WS连接 */
function fnWS() { function fnWS() {
const options: OptionsType = { const options: wsUtil.OptionsType = {
url: '/ws', url: '/ws',
params: { params: {
/**订阅通道组 /**订阅通道组
@@ -135,15 +396,167 @@ function fnWS() {
ws.connect(options); ws.connect(options);
} }
watch( // =========== WK ==============
() => state.initialized, const NO_SELECTION = { id: '', idx: 0, start: 0, length: 0 };
v => {
v && fnWS(); type StateType = {
/**初始化 */
initialized: boolean;
/**当前选中的帧编号 */
selectedFrame: number;
/**当前选中的帧数据 */
packetFrame: { tree: any[]; data_sources: any[] };
/**pcap包帧数据 */
packetFrameTreeMap: Map<string, any> | null;
/**当前选中的帧数据 */
selectedTree: typeof NO_SELECTION;
/**选择帧的Dump数据标签 */
selectedDataSourceIndex: number;
};
const state = reactive<StateType>({
initialized: false,
selectedFrame: 0,
/**当前选中的帧数据 */
packetFrame: { tree: [], data_sources: [] },
packetFrameTreeMap: null, // 注意Map 需要额外处理
selectedTree: NO_SELECTION, // NO_SELECTION 需要定义
/**选择帧的Dump数据标签 */
selectedDataSourceIndex: 0,
});
/**解析帧数据为简单结构 */
function parseFrameTree(id: string, node: Record<string, any>) {
let map = new Map();
if (node.tree && node.tree.length > 0) {
for (let i = 0; i < node.tree.length; i++) {
const subMap = parseFrameTree(`${id}-${i}`, node.tree[i]);
subMap.forEach((value, key) => {
map.set(key, value);
});
}
} else if (node.length > 0) {
map.set(id, {
id: id,
idx: node.data_source_idx,
start: node.start,
length: node.length,
});
} }
);
return map;
}
/**报文数据点击选中 */
function handleSelectedFindSelection(src_idx: number, pos: number) {
// console.log('fnSelectedFindSelection', pos);
if (state.packetFrameTreeMap == null) return;
// find the smallest one
let current = null;
for (let [k, pp] of state.packetFrameTreeMap) {
if (pp.idx !== src_idx) continue;
if (pos >= pp.start && pos <= pp.start + pp.length) {
if (
current != null &&
state.packetFrameTreeMap.get(current).length > pp.length
) {
current = k;
} else {
current = k;
}
}
}
if (current != null) {
state.selectedTree = state.packetFrameTreeMap.get(current);
}
}
/**帧数据点击选中 */
function handleSelectedTree(e: any) {
state.selectedTree = e;
}
/**接收数据后回调 */
function wkMessage(res: Record<string, any>) {
// console.log('wkMessage', res);
switch (res.type) {
case 'status':
console.info(res.status);
break;
case 'error':
console.warn(res.error);
break;
case 'init':
wk.send({ type: 'columns' });
state.initialized = true;
fnGetList();
break;
case 'frames':
const { frames } = res.data;
// 有匹配的选择第一个
if (frames.length > 0) {
state.selectedFrame = frames[0].number;
wk.send({ type: 'select', number: state.selectedFrame });
}
break;
case 'selected':
// 去掉两层
res.data.tree.shift();
res.data.tree.shift();
state.packetFrame = res.data;
state.packetFrameTreeMap = parseFrameTree('root', res.data);
state.selectedTree = NO_SELECTION;
state.selectedDataSourceIndex = 0;
break;
case 'processed':
// setStatus(`Error: non-zero return code (${e.data.code})`);
// 加载数据
wk.send({
type: 'frames',
filter: '',
skip: 0,
limit: 1,
});
break;
default:
console.warn(res);
break;
}
}
/**建立WK连接 */
function fnWK() {
const options: wkUtil.OptionsType = {
url: wkUrl,
onmessage: wkMessage,
onerror: (ev: any) => {
console.error(ev);
},
};
//建立连接
wk.connect(options);
}
onMounted(() => {
// 初始字典数据
Promise.allSettled([getDict('trace_msg_type'), getDict('trace_msg_direct')])
.then(resArr => {
if (resArr[0].status === 'fulfilled') {
dict.traceMsgType = resArr[0].value;
}
if (resArr[1].status === 'fulfilled') {
dict.traceMsgDirect = resArr[1].value;
}
})
.finally(() => {
fnWK();
fnWS();
});
});
onBeforeUnmount(() => { onBeforeUnmount(() => {
ws.close(); wk.send({ type: 'close' }) && wk.close();
if (ws.state() <= WebSocket.OPEN) ws.close();
}); });
</script> </script>
@@ -154,8 +567,9 @@ onBeforeUnmount(() => {
:loading="!state.initialized" :loading="!state.initialized"
:body-style="{ padding: '12px' }" :body-style="{ padding: '12px' }"
> >
<div class="toolbar"> <!-- 插槽-卡片左侧侧 -->
<a-space :size="8" class="toolbar-oper"> <template #title>
<a-space :size="8" align="center">
<a-button type="default" @click.prevent="fnClose()"> <a-button type="default" @click.prevent="fnClose()">
<template #icon><CloseOutlined /></template> <template #icon><CloseOutlined /></template>
{{ t('common.close') }} {{ t('common.close') }}
@@ -173,89 +587,65 @@ onBeforeUnmount(() => {
<strong>{{ traceId }}</strong> <strong>{{ traceId }}</strong>
</span> </span>
</a-space> </a-space>
</template>
<div class="toolbar-info"> <!-- 插槽-卡片右侧 -->
<a-tag color="green" v-show="!!state.currentFilter"> <template #extra>
{{ state.currentFilter }} <a-tooltip>
</a-tag> <template #title>{{ t('common.reloadText') }}</template>
<span> Matched Frame: {{ state.totalFrames }} </span> <a-button type="text" @click.prevent="fnGetList()">
</div> <template #icon><ReloadOutlined /></template>
</a-button>
<!-- 包信息 --> </a-tooltip>
<a-popover </template>
trigger="click"
placement="bottomLeft"
v-if="state.summary.filename"
>
<template #content>
<div class="summary">
<div class="summary-item">
<span>Type:</span>
<span>{{ state.summary.file_type }}</span>
</div>
<div class="summary-item">
<span>Encapsulation:</span>
<span>{{ state.summary.file_encap_type }}</span>
</div>
<div class="summary-item">
<span>Packets:</span>
<span>{{ state.summary.packet_count }}</span>
</div>
<div class="summary-item">
<span>Duration:</span>
<span>{{ Math.round(state.summary.elapsed_time) }}s</span>
</div>
</div>
</template>
<InfoCircleOutlined />
</a-popover>
</div>
<!-- 包数据表过滤 -->
<a-input-group compact>
<a-input
v-model:value="state.filter"
placeholder="display filter, example: tcp"
:allow-clear="true"
style="width: calc(100% - 100px)"
@pressEnter="handleFilterFrames"
>
<template #prefix>
<FilterOutlined />
</template>
</a-input>
<a-button
type="primary"
html-type="submit"
style="width: 100px"
@click="handleFilterFrames"
>
Filter
</a-button>
</a-input-group>
<a-alert
:message="state.filterError"
type="error"
v-if="state.filterError != null"
/>
<!-- 包数据表 --> <!-- 包数据表 -->
<PacketTable <a-table
:columns="state.columns" class="table"
:data="state.packetFrames" row-key="id"
:selectedFrame="state.selectedFrame" :columns="tableColumns"
:onSelectedFrame="handleSelectedFrame" :loading="tableState.loading"
:onScrollBottom="handleScrollBottom" :data-source="tableState.data"
></PacketTable> :size="tableState.size"
:pagination="tablePagination"
<a-row> :row-class-name="(record:any) => {
if (record.id === tableState.row.id) {
return 'table-striped-select';
}
return record.msgDirect === 0 ? 'table-striped-recv' : 'table-striped-send';
}"
:customRow="
record => {
return {
onClick: () => fnVisible(record),
};
}
"
@change="fnTableChange"
:scroll="{ x: tableColumns.length * 150, y: 'calc(100vh - 300px)' }"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'msgType'">
<DictTag :options="dict.traceMsgType" :value="record.msgType" />
</template>
<template v-if="column.key === 'msgDirect'">
<DictTag :options="dict.traceMsgDirect" :value="record.msgDirect" />
</template>
</template>
</a-table>
<!-- 帧数据 -->
<a-row
:gutter="16"
style="border: 2px rgb(217 217 217) solid; border-radius: 6px"
v-show="state.selectedFrame == 1"
>
<a-col :lg="12" :md="12" :xs="24" class="tree"> <a-col :lg="12" :md="12" :xs="24" class="tree">
<!-- 帧数据 --> <!-- 帧数据 -->
<DissectionTree <DissectionTree
id="root" id="root"
:select="handleSelectedTreeEntry" :select="handleSelectedTree"
:selected="state.selectedTreeEntry" :selected="state.selectedTree"
:tree="state.selectedPacket.tree" :tree="state.packetFrame.tree"
/> />
</a-col> </a-col>
<a-col :lg="12" :md="12" :xs="24" class="dump"> <a-col :lg="12" :md="12" :xs="24" class="dump">
@@ -268,15 +658,15 @@ onBeforeUnmount(() => {
<a-tab-pane <a-tab-pane
:key="idx" :key="idx"
:tab="v.name" :tab="v.name"
v-for="(v, idx) in state.selectedPacket.data_sources" v-for="(v, idx) in state.packetFrame.data_sources"
style="overflow: auto" style="overflow: auto"
> >
<DissectionDump <DissectionDump
:base64="v.data" :base64="v.data"
:select="(pos:number)=>handleSelectedFindSelection(idx, pos)" :select="(pos:number)=>handleSelectedFindSelection(idx, pos)"
:selected=" :selected="
idx === state.selectedTreeEntry.idx idx === state.selectedTree.idx
? state.selectedTreeEntry ? state.selectedTree
: NO_SELECTION : NO_SELECTION
" "
/> />
@@ -289,24 +679,20 @@ onBeforeUnmount(() => {
</template> </template>
<style scoped> <style scoped>
.toolbar { .table :deep(.ant-pagination) {
display: flex; padding: 0 24px;
align-items: center;
margin-bottom: 12px;
} }
.toolbar-info { .table :deep(.table-striped-select) td {
flex: 1; background-color: #c2c2c2;
text-align: right; cursor: pointer;
padding-right: 8px;
} }
.table :deep(.table-striped-recv) td {
.summary { background-color: #a9e2ff;
display: flex; cursor: pointer;
flex-direction: column;
} }
.summary-item > span:first-child { .table :deep(.table-striped-send) td {
font-weight: 600; background-color: #bcfbb3;
margin-right: 6px; cursor: pointer;
} }
.tree { .tree {

View File

@@ -1,24 +1,60 @@
<script setup lang="ts"> <script setup lang="ts">
import { reactive, onMounted, toRaw } from 'vue'; import { reactive, onMounted, toRaw, ref } from 'vue';
import { PageContainer } from 'antdv-pro-layout'; import { PageContainer } from 'antdv-pro-layout';
import { ProModal } from 'antdv-pro-modal'; import { ProModal } from 'antdv-pro-modal';
import { Modal } from 'ant-design-vue/es';
import { SizeType } from 'ant-design-vue/es/config-provider'; import { SizeType } from 'ant-design-vue/es/config-provider';
import { MenuInfo } from 'ant-design-vue/es/menu/src/interface'; import { MenuInfo } from 'ant-design-vue/es/menu/src/interface';
import { ColumnsType } from 'ant-design-vue/es/table'; import { ColumnsType } from 'ant-design-vue/es/table';
import { parseDateToStr } from '@/utils/date-utils'; import { parseDateToStr } from '@/utils/date-utils';
import { RESULT_CODE_SUCCESS } from '@/constants/result-constants'; import { RESULT_CODE_SUCCESS } from '@/constants/result-constants';
import { saveAs } from 'file-saver'; import useDictStore from '@/store/modules/dict';
import useTabsStore from '@/store/modules/tabs';
import { type Dayjs } from 'dayjs';
import useI18n from '@/hooks/useI18n'; import useI18n from '@/hooks/useI18n';
import { listTraceData } from '@/api/trace/analysis'; import { getTraceData, listTraceData } from '@/api/trace/task';
import { decode } from 'js-base64';
import { useRoute, useRouter } from 'vue-router';
const { t } = useI18n(); const { t } = useI18n();
const { getDict } = useDictStore();
const tabsStore = useTabsStore();
const router = useRouter();
const route = useRoute();
/**跟踪编号 */
const traceId = ref<string>(route.query.traceId as string);
/**关闭跳转 */
function fnClose() {
const to = tabsStore.tabClose(route.path);
if (to) {
router.push(to);
} else {
router.back();
}
}
/**字典数据 */
let dict: {
/**跟踪消息类型 */
traceMsgType: DictType[];
/**跟踪消息方向 */
traceMsgDirect: DictType[];
} = reactive({
traceMsgType: [],
traceMsgDirect: [],
});
/**开始结束时间 */
let queryRangePicker = ref<[Dayjs, Dayjs] | undefined>(undefined);
/**查询参数 */ /**查询参数 */
let queryParams = reactive({ let queryParams = reactive({
/**移动号 */ traceId: traceId.value,
imsi: '', sortBy: 'timestamp',
/**移动号 */ sortOrder: 'asc',
msisdn: '', /**开始时间 */
beginTime: undefined as undefined | number,
/**结束时间 */
endTime: undefined as undefined | number,
/**当前页数 */ /**当前页数 */
pageNum: 1, pageNum: 1,
/**每页条数 */ /**每页条数 */
@@ -28,10 +64,10 @@ let queryParams = reactive({
/**查询参数重置 */ /**查询参数重置 */
function fnQueryReset() { function fnQueryReset() {
queryParams = Object.assign(queryParams, { queryParams = Object.assign(queryParams, {
imsi: '',
pageNum: 1, pageNum: 1,
pageSize: 20, pageSize: 20,
}); });
queryRangePicker.value = undefined;
tablePagination.current = 1; tablePagination.current = 1;
tablePagination.pageSize = 20; tablePagination.pageSize = 20;
fnGetList(); fnGetList();
@@ -43,8 +79,6 @@ type TabeStateType = {
loading: boolean; loading: boolean;
/**紧凑型 */ /**紧凑型 */
size: SizeType; size: SizeType;
/**搜索栏 */
seached: boolean;
/**记录数据 */ /**记录数据 */
data: object[]; data: object[];
}; };
@@ -52,66 +86,87 @@ type TabeStateType = {
/**表格状态 */ /**表格状态 */
let tableState: TabeStateType = reactive({ let tableState: TabeStateType = reactive({
loading: false, loading: false,
size: 'middle', size: 'small',
seached: true,
data: [], data: [],
}); });
/**表格字段列 */ /**表格字段列 */
let tableColumns: ColumnsType = [ let tableColumns: ColumnsType = [
{ {
title: t('views.traceManage.analysis.trackTaskId'), title: t('common.rowId'),
dataIndex: 'taskId', dataIndex: 'id',
align: 'center', align: 'left',
width: 100,
}, },
{ {
title: t('views.traceManage.analysis.imsi'), title: t('views.traceManage.task.msgNe'),
dataIndex: 'imsi', dataIndex: 'msgNe',
align: 'center', align: 'left',
width: 150,
}, },
{ {
title: t('views.traceManage.analysis.msisdn'), title: t('views.traceManage.task.rowTime'),
dataIndex: 'msisdn',
align: 'center',
},
{
title: t('views.traceManage.analysis.srcIp'),
dataIndex: 'srcAddr',
align: 'center',
},
{
title: t('views.traceManage.analysis.dstIp'),
dataIndex: 'dstAddr',
align: 'center',
},
{
title: t('views.traceManage.analysis.signalType'),
dataIndex: 'ifType',
align: 'center',
},
{
title: t('views.traceManage.analysis.msgType'),
dataIndex: 'msgType',
align: 'center',
},
{
title: t('views.traceManage.analysis.msgDirect'),
dataIndex: 'msgDirect',
align: 'center',
},
{
title: t('views.traceManage.analysis.rowTime'),
dataIndex: 'timestamp', dataIndex: 'timestamp',
align: 'center', align: 'left',
width: 250,
customRender(opt) { customRender(opt) {
if (!opt.value) return ''; if (!opt.value) return '';
return parseDateToStr(opt.value); const nanoseconds = opt.value; //
const milliseconds = Math.floor(nanoseconds / 1000000);
const nanosecondsPart = (nanoseconds % 1000000)
.toString()
.padStart(6, '0');
return `${parseDateToStr(
milliseconds,
'YYYY-MM-DD HH:mm:ss.SSS'
)}.${nanosecondsPart}`;
}, },
sorter: true,
},
{
title: t('views.traceManage.task.protocolOrInterface'),
dataIndex: 'ifType',
align: 'left',
width: 100,
},
{
title: t('views.traceManage.task.msgEvent'),
dataIndex: 'msgEvent',
align: 'left',
width: 200,
ellipsis: true,
},
{
title: t('views.traceManage.task.msgType'),
dataIndex: 'msgType',
key: 'msgType',
align: 'left',
width: 100,
},
{
title: t('views.traceManage.task.msgDirect'),
dataIndex: 'msgDirect',
key: 'msgDirect',
align: 'left',
width: 100,
},
{
title: t('views.traceManage.task.srcIp'),
dataIndex: 'srcAddr',
align: 'left',
width: 150,
},
{
title: t('views.traceManage.task.dstIp'),
dataIndex: 'dstAddr',
align: 'left',
width: 200,
}, },
{ {
title: t('common.operate'), title: t('common.operate'),
key: 'id', key: 'id',
align: 'center', align: 'left',
}, },
]; ];
@@ -148,6 +203,18 @@ function fnTableSize({ key }: MenuInfo) {
tableState.size = key as SizeType; tableState.size = key as SizeType;
} }
/**表格分页、排序、筛选变化时触发操作, 排序方式,取值为 ascend descend */
function fnTableChange(pagination: any, filters: any, sorter: any, extra: any) {
const { field, order } = sorter;
if (order) {
queryParams.sortBy = field;
queryParams.sortOrder = order.replace('end', '');
} else {
queryParams.sortOrder = 'asc';
}
fnGetList(1);
}
/**查询备份信息列表, pageNum初始页数 */ /**查询备份信息列表, pageNum初始页数 */
function fnGetList(pageNum?: number) { function fnGetList(pageNum?: number) {
if (tableState.loading) return; if (tableState.loading) return;
@@ -155,6 +222,19 @@ function fnGetList(pageNum?: number) {
if (pageNum) { if (pageNum) {
queryParams.pageNum = pageNum; queryParams.pageNum = pageNum;
} }
//
if (
Array.isArray(queryRangePicker.value) &&
queryRangePicker.value.length > 0
) {
queryParams.beginTime = queryRangePicker.value[0].valueOf();
queryParams.endTime = queryRangePicker.value[1].valueOf();
} else {
queryParams.beginTime = undefined;
queryParams.endTime = undefined;
}
listTraceData(toRaw(queryParams)).then(res => { listTraceData(toRaw(queryParams)).then(res => {
if (res.code === RESULT_CODE_SUCCESS) { if (res.code === RESULT_CODE_SUCCESS) {
const { total, rows } = res.data; const { total, rows } = res.data;
@@ -199,24 +279,17 @@ let modalState: ModalStateType = reactive({
* @param row 记录信息 * @param row 记录信息
*/ */
function fnModalVisible(row: Record<string, any>) { function fnModalVisible(row: Record<string, any>) {
// getTraceData(row.id).then(res => {
const hexString = parseBase64Data(row.rawMsg); if (res.code === RESULT_CODE_SUCCESS) {
const rawData = convertToReadableFormat(hexString); Object.assign(modalState.from, res.data);
modalState.from.rawData = rawData; //
// RAWHTML const hexString = parseBase64Data(res.data.rawMsg);
// getTraceRawInfo(row.id).then(res => { const rawData = convertToReadableFormat(hexString);
// if (res.code === RESULT_CODE_SUCCESS) { modalState.from.rawData = rawData;
// const htmlString = rawDataHTMLScript(res.msg); modalState.title = t('views.traceManage.task.taskInfo');
// modalState.from.rawDataHTML = htmlString; modalState.open = true;
// modalState.from.downBtn = true; }
// } else {
// modalState.from.rawDataHTML = t('views.traceManage.analysis.noData');
// }
// });
modalState.title = t('views.traceManage.analysis.taskTitle', {
num: row.imsi,
}); });
modalState.open = true;
} }
/** /**
@@ -224,15 +297,13 @@ function fnModalVisible(row: Record<string, any>) {
*/ */
function fnModalVisibleClose() { function fnModalVisibleClose() {
modalState.open = false; modalState.open = false;
modalState.from.downBtn = false;
modalState.from.rawDataHTML = '';
modalState.from.rawData = ''; modalState.from.rawData = '';
} }
// Base64 // Base64
function parseBase64Data(hexData: string) { function parseBase64Data(base64Data: string) {
// Base64 // Base64
const byteString = atob(hexData); const byteString = decode(base64Data);
const byteArray = new Uint8Array(byteString.length); const byteArray = new Uint8Array(byteString.length);
for (let i = 0; i < byteString.length; i++) { for (let i = 0; i < byteString.length; i++) {
byteArray[i] = byteString.charCodeAt(i); byteArray[i] = byteString.charCodeAt(i);
@@ -252,7 +323,7 @@ function convertToReadableFormat(hexString: string) {
let result = ''; let result = '';
let asciiResult = ''; let asciiResult = '';
let arr = []; let arr = [];
let row = 100; let row = 10000;
for (let i = 0; i < hexString.length; i += 2) { for (let i = 0; i < hexString.length; i += 2) {
const hexChars = hexString.substring(i, i + 2); const hexChars = hexString.substring(i, i + 2);
const decimal = parseInt(hexChars, 16); const decimal = parseInt(hexChars, 16);
@@ -286,100 +357,46 @@ function convertToReadableFormat(hexString: string) {
return arr; return arr;
} }
// HTMl
function rawDataHTMLScript(htmlString: string) {
// <a>
// const withoutATags = htmlString.replace(/<a\b[^>]*>(.*?)<\/a>/gi, '');
// <script>
let withoutScriptTags = htmlString.replace(
/<script\b[^>]*>([\s\S]*?)<\/script>/gi,
''
);
//
// const withoutHiddenElements = withoutScriptTags.replace(
// /style="display:none"/gi,
// 'style="background:#ffffff"'
// );
function set_node(node: any, str: string) {
if (!node) return;
node.style.display = str;
node.style.background = '#ffffff';
}
Reflect.set(window, 'set_node', set_node);
function toggle_node(node: any) {
node = document.getElementById(node);
if (!node) return;
set_node(node, node.style.display != 'none' ? 'none' : 'block');
}
Reflect.set(window, 'toggle_node', toggle_node);
function hide_node(node: any) {
node = document.getElementById(node);
if (!node) return;
set_node(node, 'none');
}
Reflect.set(window, 'hide_node', hide_node);
//
withoutScriptTags = withoutScriptTags.replace(
'id="f1c" style="display:none"',
'id="f1c" style="display:block"'
);
return withoutScriptTags;
}
/**信息文件下载 */
function fnDownloadFile() {
Modal.confirm({
title: t('common.tipTitle'),
content: t('views.traceManage.analysis.taskDownTip'),
onOk() {
const blob = new Blob([modalState.from.rawDataHTML], {
type: 'text/plain',
});
saveAs(blob, `${modalState.title}_${Date.now()}.html`);
},
});
}
onMounted(() => { onMounted(() => {
// //
fnGetList(); Promise.allSettled([getDict('trace_msg_type'), getDict('trace_msg_direct')])
.then(resArr => {
if (resArr[0].status === 'fulfilled') {
dict.traceMsgType = resArr[0].value;
}
if (resArr[1].status === 'fulfilled') {
dict.traceMsgDirect = resArr[1].value;
}
})
.finally(() => {
//
fnGetList();
});
}); });
</script> </script>
<template> <template>
<PageContainer> <PageContainer>
<a-card <a-card
v-show="tableState.seached"
:bordered="false" :bordered="false"
:body-style="{ marginBottom: '24px', paddingBottom: 0 }" :body-style="{ marginBottom: '24px', paddingBottom: 0 }"
> >
<!-- 表格搜索栏 --> <!-- 表格搜索栏 -->
<a-form :model="queryParams" name="queryParams" layout="horizontal"> <a-form :model="queryParams" name="queryParams" layout="horizontal">
<a-row :gutter="16"> <a-row :gutter="16">
<a-col :lg="6" :md="12" :xs="24"> <a-col :lg="8" :md="12" :xs="24">
<a-form-item <a-form-item
:label="t('views.traceManage.analysis.imsi')" :label="t('views.traceManage.task.rowTime')"
name="imsi" name="queryRangePicker"
> >
<a-input <a-range-picker
v-model:value="queryParams.imsi" v-model:value="queryRangePicker"
:allow-clear="true" :bordered="true"
:placeholder="t('views.traceManage.analysis.imsiPlease')" :allow-clear="false"
></a-input> style="width: 100%"
</a-form-item> :show-time="{ format: 'HH:mm:ss' }"
</a-col> format="YYYY-MM-DD HH:mm:ss"
<a-col :lg="6" :md="12" :xs="24"> ></a-range-picker>
<a-form-item
:label="t('views.traceManage.analysis.msisdn')"
name="imsi"
>
<a-input
v-model:value="queryParams.msisdn"
:allow-clear="true"
:placeholder="t('views.traceManage.analysis.msisdnPlease')"
></a-input>
</a-form-item> </a-form-item>
</a-col> </a-col>
<a-col :lg="6" :md="12" :xs="24"> <a-col :lg="6" :md="12" :xs="24">
@@ -402,20 +419,22 @@ onMounted(() => {
<a-card :bordered="false" :body-style="{ padding: '0px' }"> <a-card :bordered="false" :body-style="{ padding: '0px' }">
<!-- 插槽-卡片左侧侧 --> <!-- 插槽-卡片左侧侧 -->
<template #title> </template> <template #title>
<a-space :size="8" align="center">
<a-button type="default" @click.prevent="fnClose()">
<template #icon><CloseOutlined /></template>
{{ t('common.close') }}
</a-button>
<span>
{{ t('views.traceManage.task.traceId') }}:&nbsp;
<strong>{{ traceId }}</strong>
</span>
</a-space>
</template>
<!-- 插槽-卡片右侧 --> <!-- 插槽-卡片右侧 -->
<template #extra> <template #extra>
<a-space :size="8" align="center"> <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> <a-tooltip>
<template #title>{{ t('common.reloadText') }}</template> <template #title>{{ t('common.reloadText') }}</template>
<a-button type="text" @click.prevent="fnGetList()"> <a-button type="text" @click.prevent="fnGetList()">
@@ -459,11 +478,18 @@ onMounted(() => {
:size="tableState.size" :size="tableState.size"
:pagination="tablePagination" :pagination="tablePagination"
:scroll="{ x: true }" :scroll="{ x: true }"
@change="fnTableChange"
> >
<template #bodyCell="{ column, record }"> <template #bodyCell="{ column, record }">
<template v-if="column.key === 'msgType'">
<DictTag :options="dict.traceMsgType" :value="record.msgType" />
</template>
<template v-if="column.key === 'msgDirect'">
<DictTag :options="dict.traceMsgDirect" :value="record.msgDirect" />
</template>
<template v-if="column.key === 'id'"> <template v-if="column.key === 'id'">
<a-space :size="8" align="center"> <a-space :size="8" align="center">
<a-tooltip> <a-tooltip placement="topRight">
<template #title>{{ t('common.viewText') }}</template> <template #title>{{ t('common.viewText') }}</template>
<a-button type="link" @click.prevent="fnModalVisible(record)"> <a-button type="link" @click.prevent="fnModalVisible(record)">
<template #icon><ProfileOutlined /></template> <template #icon><ProfileOutlined /></template>
@@ -482,31 +508,111 @@ onMounted(() => {
:title="modalState.title" :title="modalState.title"
:open="modalState.open" :open="modalState.open"
@cancel="fnModalVisibleClose" @cancel="fnModalVisibleClose"
:footer="false"
> >
<div class="raw-title"> <a-form
{{ t('views.traceManage.analysis.signalData') }} name="modalStateFrom"
</div> layout="horizontal"
<a-row class="raw" v-for="v in modalState.from.rawData" :key="v.row"> :label-col="{ span: 8 }"
<a-col class="num" :span="2">{{ v.row }}</a-col> :label-wrap="true"
<a-col class="code" :span="12">{{ v.code }}</a-col> >
<a-col class="txt" :span="10">{{ v.asciiText }}</a-col> <a-row>
</a-row> <a-col :lg="12" :md="12" :xs="24">
<a-divider /> <a-form-item
<!-- <div class="raw-title"> :label="t('views.traceManage.task.msgType')"
{{ t('views.traceManage.analysis.signalDetail') }} name="msgType"
<a-button >
type="dashed" <DictTag
size="small" :options="dict.traceMsgType"
@click.prevent="fnDownloadFile" :value="modalState.from.msgType"
v-if="modalState.from.downBtn" />
</a-form-item>
</a-col>
<a-col :lg="12" :md="12" :xs="24">
<a-form-item
:label="t('views.traceManage.task.msgDirect')"
name="msgDirect"
>
<DictTag
:options="dict.traceMsgDirect"
:value="modalState.from.msgDirect"
/>
</a-form-item>
</a-col>
</a-row>
<a-row>
<a-col :lg="12" :md="12" :xs="24">
<a-form-item
:label="t('views.traceManage.task.srcIp')"
name="srcAddr"
>
{{ modalState.from.srcAddr }}
</a-form-item>
</a-col>
<a-col :lg="12" :md="12" :xs="24">
<a-form-item
:label="t('views.traceManage.task.dstIp')"
name="dstAddr"
>
{{ modalState.from.dstAddr }}
</a-form-item>
</a-col>
</a-row>
<a-row>
<a-col :lg="12" :md="12" :xs="24">
<a-form-item
:label="t('views.traceManage.task.msgNe')"
name="msgNe"
>
{{ modalState.from.msgNe }}
</a-form-item>
</a-col>
<a-col :lg="12" :md="12" :xs="24">
<a-form-item
:label="t('views.traceManage.task.msgEvent')"
name="msgEvent"
>
{{ modalState.from.msgEvent }}
</a-form-item>
</a-col>
</a-row>
<a-form-item
v-if="modalState.from.imsi"
:label="t('views.traceManage.task.imsi')"
name="imsi"
:label-col="{ span: 4 }"
> >
<template #icon> {{ modalState.from.imsi }}
<DownloadOutlined /> </a-form-item>
</template>
{{ t('views.traceManage.analysis.taskDownText') }} <a-form-item
</a-button> v-if="modalState.from.ifType"
</div> --> :label="t('views.traceManage.task.protocolOrInterface')"
<!-- <div class="raw-html" v-html="modalState.from.rawDataHTML"></div> --> name="ifType"
:label-col="{ span: 4 }"
>
{{ modalState.from.ifType }}
</a-form-item>
<a-form-item
:label="t('views.traceManage.task.msgLen')"
name="length"
:label-col="{ span: 4 }"
>
{{ modalState.from.length }}
</a-form-item>
<a-form-item>
<a-row class="raw" v-for="v in modalState.from.rawData" :key="v.row">
<a-col class="num" :span="2">{{ v.row }}</a-col>
<a-col class="code" :span="12">{{ v.code }}</a-col>
<a-col class="txt" :span="10">{{ v.asciiText }}</a-col>
</a-row>
</a-form-item>
</a-form>
</ProModal> </ProModal>
</PageContainer> </PageContainer>
</template> </template>
@@ -517,24 +623,15 @@ onMounted(() => {
} }
.raw { .raw {
&-title {
color: #000000d9;
font-size: 24px;
line-height: 1.8;
}
.num { .num {
background-color: #e5e5e5; background-color: #f0f0f0;
} }
.code { .code {
background-color: #e7e6ff; background-color: #0078d4;
color: #fff;
} }
.txt { .txt {
background-color: #ffe3e5; background-color: #d9d9d9;
}
&-html {
max-height: 300px;
overflow-y: auto;
} }
} }
</style> </style>

View File

@@ -20,7 +20,7 @@ import {
updateTraceTask, updateTraceTask,
} from '@/api/trace/task'; } from '@/api/trace/task';
import useDictStore from '@/store/modules/dict'; import useDictStore from '@/store/modules/dict';
import { regExpIPv4, regExpPort } from '@/utils/regular-utils'; import { regExpIPv4 } from '@/utils/regular-utils';
import dayjs, { Dayjs } from 'dayjs'; import dayjs, { Dayjs } from 'dayjs';
import { parseObjHumpToLine } from '@/utils/parse-utils'; import { parseObjHumpToLine } from '@/utils/parse-utils';
const neInfoStore = useNeInfoStore(); const neInfoStore = useNeInfoStore();
@@ -33,8 +33,11 @@ const route = useRoute();
let dict: { let dict: {
/**跟踪类型 */ /**跟踪类型 */
traceType: DictType[]; traceType: DictType[];
/**跟踪接口 */
traceInterfaces: DictType[];
} = reactive({ } = reactive({
traceType: [], traceType: [],
traceInterfaces: [],
}); });
/**网元类型_多neId */ /**网元类型_多neId */
@@ -98,40 +101,28 @@ let tableState: TabeStateType = reactive({
/**表格字段列 */ /**表格字段列 */
let tableColumns: ColumnsType = [ let tableColumns: ColumnsType = [
{
title: t('views.ne.common.neType'),
dataIndex: 'neType',
align: 'left',
sorter: {
compare: (a, b) => 1,
multiple: 1,
},
},
{
title: t('views.ne.common.neId'),
dataIndex: 'neId',
align: 'left',
},
{ {
title: t('views.traceManage.task.traceId'), title: t('views.traceManage.task.traceId'),
dataIndex: 'traceId', dataIndex: 'traceId',
align: 'left', align: 'left',
width: 150,
}, },
{ {
title: t('views.traceManage.task.trackType'), title: t('views.traceManage.task.trackType'),
dataIndex: 'traceType', dataIndex: 'traceType',
key: 'traceType', key: 'traceType',
align: 'left', align: 'left',
width: 150,
}, },
{ {
title: t('views.traceManage.task.startTime'), title: t('views.traceManage.task.startTime'),
dataIndex: 'startTime', dataIndex: 'startTime',
align: 'left', align: 'left',
width: 200,
customRender(opt) { customRender(opt) {
if (!opt.value) return ''; if (!opt.value) return '';
return parseDateToStr(opt.value); return parseDateToStr(opt.value);
}, },
sorter: true,
}, },
{ {
title: t('views.traceManage.task.endTime'), title: t('views.traceManage.task.endTime'),
@@ -141,6 +132,7 @@ let tableColumns: ColumnsType = [
if (!opt.value) return ''; if (!opt.value) return '';
return parseDateToStr(opt.value); return parseDateToStr(opt.value);
}, },
width: 200,
}, },
{ {
title: t('common.operate'), title: t('common.operate'),
@@ -274,18 +266,15 @@ type ModalStateType = {
/**标题 */ /**标题 */
title: string; title: string;
/**网元类型设备对象 */ /**网元类型设备对象 */
neType: string[]; neType: any[] | undefined;
/**网元类型设备对象接口 */ /**网元类型设备对象接口 */
neTypeInterface: Record<string, any>[]; neTypeInterface: string[];
/**网元类型设备对象接口选择 */
neTypeInterfaceSelect: string[];
/**任务开始结束时间 */ /**任务开始结束时间 */
timeRangePicker: [string, string]; timeRangePicker: [Dayjs, Dayjs] | undefined;
/**表单数据 */ /**表单数据 */
from: { from: {
id?: string; id?: string;
neType: string; neList: string; // 网元列表 neType_neId 例如 UDM_001,AMF_001
neId: string;
/**1-Interface,2-Device,3-User */ /**1-Interface,2-Device,3-User */
traceType: string; traceType: string;
startTime?: number; startTime?: number;
@@ -312,13 +301,11 @@ let modalState: ModalStateType = reactive({
title: '', title: '',
neType: [], neType: [],
neTypeInterface: [], neTypeInterface: [],
neTypeInterfaceSelect: [], timeRangePicker: undefined,
timeRangePicker: ['', ''],
from: { from: {
id: undefined, id: undefined,
neType: '', neList: '',
neId: '', traceId: undefined,
traceId: '',
traceType: '3', traceType: '3',
startTime: undefined, startTime: undefined,
endTime: undefined, endTime: undefined,
@@ -330,8 +317,8 @@ let modalState: ModalStateType = reactive({
dstIp: '', dstIp: '',
signalPort: undefined, signalPort: undefined,
/**3用户跟踪 */ /**3用户跟踪 */
imsi: '', imsi: undefined,
msisdn: '', // msisdn: undefined,
}, },
confirmLoading: false, confirmLoading: false,
}); });
@@ -346,7 +333,7 @@ const modalStateFrom = Form.useForm(
message: t('views.traceManage.task.trackTypePlease'), message: t('views.traceManage.task.trackTypePlease'),
}, },
], ],
neId: [ neList: [
{ {
required: true, required: true,
message: t('views.ne.common.neTypePlease'), message: t('views.ne.common.neTypePlease'),
@@ -393,43 +380,48 @@ const modalStateFrom = Form.useForm(
message: t('views.traceManage.task.dstIpPlease'), message: t('views.traceManage.task.dstIpPlease'),
}, },
], ],
signalPort: [
{
required: false,
pattern: regExpPort,
message: t('views.traceManage.task.signalPortPlease'),
},
],
}) })
); );
/**网元类型选择对应修改 */ /**网元类型选择对应修改 */
function fnNeChange(_: any, item: any) { function fnNeChange(p: any, c: any) {
modalState.from.neType = item[1].neType; let neList: string[] = [];
modalState.from.neId = item[1].neId; for (let i = 0; i < p.length; i++) {
// 网元信令接口可选列表 const v = p[i];
modalState.from.interfaces = ''; if (v.length === 1) {
modalState.neTypeInterfaceSelect = []; c[i][0].children.forEach((item: any) => {
fnSelectInterfaceInit(item[1].neType); neList.push(`${item.neType}_${item.neId}`);
} });
} else if (v.length === 2) {
/**跟踪类型选择对应修改 */ neList.push(`${v[0]}_${v[1]}`);
function fnTraceTypeChange(v: any, _: any) { }
// 网元信令接口可选列表 }
if (v === '1' && modalState.from.neType) { if (neList.length > 0) {
modalState.from.interfaces = ''; modalState.from.neList = neList.join(',');
modalState.neTypeInterfaceSelect = []; } else {
fnSelectInterfaceInit(modalState.from.neType); modalState.from.neList = '';
} }
} }
/**开始结束时间选择对应修改 */ /**开始结束时间选择对应修改 */
function fnRangePickerChange(item: any, _: any) { function fnRangePickerChange(item: any, _: any) {
modalState.from.startTime = +item[0]; if (!item || item.length !== 2) {
modalState.from.endTime = +item[1]; modalState.from.startTime = undefined;
modalState.from.endTime = undefined;
return;
}
// 获取当前时间
const now = dayjs();
// 如果开始时间小于当前时间,则设置为当前时间
const startTime = item[0].isBefore(now) ? now : item[0];
const endTime = item[1].isBefore(now) ? now : item[1];
modalState.timeRangePicker = [startTime, endTime];
modalState.from.startTime = startTime.valueOf();
modalState.from.endTime = endTime.valueOf();
} }
function fnRangePickerDisabledDate(current: Dayjs) { function fnRangePickerDisabledDate(current: Dayjs) {
return current && current < dayjs().subtract(1, 'day').endOf('day'); return current && current < dayjs().startOf('day');
} }
/**信令接口选择对应修改 */ /**信令接口选择对应修改 */
@@ -437,19 +429,6 @@ function fnSelectInterface(s: any, _: any) {
modalState.from.interfaces = s.join(','); modalState.from.interfaces = s.join(',');
} }
/**信令接口选择初始 */
function fnSelectInterfaceInit(neType: string) {
const interfaces = neInfoStore.traceInterfaceList;
modalState.neTypeInterface = interfaces
.filter(i => i.neType === neType)
.map(i => {
return {
value: i.interface,
label: i.interface,
};
});
}
/** /**
* 对话框弹出显示为 新增或者修改 * 对话框弹出显示为 新增或者修改
* @param noticeId 网元id, 不传为新增 * @param noticeId 网元id, 不传为新增
@@ -467,20 +446,45 @@ function fnModalOpenByEdit(id?: string) {
modalState.confirmLoading = false; modalState.confirmLoading = false;
hide(); hide();
if (res.code === RESULT_CODE_SUCCESS && res.data) { if (res.code === RESULT_CODE_SUCCESS && res.data) {
modalState.neType = [res.data.neType, res.data.neId]; // 回显网元类型
modalState.timeRangePicker = [res.data.startTime, res.data.endTime]; const neType: any[] = [];
modalState.from = Object.assign(modalState.from, res.data); const neList = res.data.neList.split(',');
// 接口 const neListMap: any = {};
if (res.data.traceType === 'Interface') { for (const v of neList) {
if ( const item: string[] = v.split('_');
res.data.interfaces.length > 4 && if (!neListMap[item[0]]) {
res.data.interfaces.includes('[') neListMap[item[0]] = [];
) {
modalState.neTypeInterfaceSelect = JSON.parse(res.data.interfaces);
} }
fnSelectInterfaceInit(res.data.neType); neListMap[item[0]].push(item[1]);
} }
modalState.title = t('views.traceManage.task.editTask'); for (const op of neCascaderOptions.value) {
const arr = neListMap[op.value];
if (!arr) {
continue;
}
const all = op.children.every((c: any) => {
return arr.includes(c.neId);
});
if (all) {
neType.push([op.value]);
} else {
arr.forEach((v: string) => {
neType.push([op.value, v]);
});
}
}
modalState.neType = neType;
// 回显时间
modalState.timeRangePicker = [
dayjs(res.data.startTime),
dayjs(res.data.endTime),
];
// 回显接口
if (res.data.traceType === '1') {
modalState.neTypeInterface = res.data.interfaces.split(',');
}
modalState.from = Object.assign(modalState.from, res.data);
modalState.title = t('views.traceManage.task.viewTask');
modalState.openByEdit = true; modalState.openByEdit = true;
} else { } else {
message.error(t('views.traceManage.task.errorTaskInfo'), 3); message.error(t('views.traceManage.task.errorTaskInfo'), 3);
@@ -495,15 +499,15 @@ function fnModalOpenByEdit(id?: string) {
*/ */
function fnModalOk() { function fnModalOk() {
const from = toRaw(modalState.from); const from = toRaw(modalState.from);
let valids = ['traceType', 'neId', 'endTime']; let valids = ['traceType', 'neList', 'endTime'];
if (from.traceType === '1') { if (from.traceType === '1') {
valids = valids.concat(['interfaces']); valids = valids.concat(['interfaces']);
} }
if (from.traceType === '2') { if (from.traceType === '2') {
valids = valids.concat(['srcIp', 'dstIp', 'signalPort']); valids = valids.concat(['srcIp', 'dstIp']);
} }
if (from.traceType === '3') { if (from.traceType === '3') {
valids = valids.concat(['imsi', 'msisdn']); valids = valids.concat(['imsi']);
} }
modalStateFrom modalStateFrom
@@ -546,29 +550,31 @@ function fnModalCancel() {
modalState.openByEdit = false; modalState.openByEdit = false;
modalState.confirmLoading = false; modalState.confirmLoading = false;
modalStateFrom.resetFields(); modalStateFrom.resetFields();
modalState.timeRangePicker = ['', ''];
modalState.neTypeInterfaceSelect = [];
modalState.neType = []; modalState.neType = [];
modalState.neTypeInterface = []; modalState.neTypeInterface = [];
modalState.timeRangePicker = undefined;
} }
/**跳转PCAP文件详情页面 */ /**跳转内嵌详情页面 */
function fnRecordPCAPView(row: Record<string, any>) { function fnRecordView(traceId: any, type: 'analyze' | 'data') {
router.push({ router.push({
path: `${route.path}${MENU_PATH_INLINE}/analyze`, path: `${route.path}${MENU_PATH_INLINE}/${type}`,
query: { query: { traceId },
traceId: row.traceId,
},
}); });
} }
onMounted(() => { onMounted(() => {
// 初始字典数据 // 初始字典数据
Promise.allSettled([getDict('trace_type')]).then(resArr => { Promise.allSettled([getDict('trace_type'), getDict('trace_interfaces')]).then(
if (resArr[0].status === 'fulfilled') { resArr => {
dict.traceType = resArr[0].value; if (resArr[0].status === 'fulfilled') {
dict.traceType = resArr[0].value;
}
if (resArr[1].status === 'fulfilled') {
dict.traceInterfaces = resArr[1].value;
}
} }
}); );
// 获取网元网元列表 // 获取网元网元列表
useNeInfoStore() useNeInfoStore()
@@ -579,13 +585,13 @@ onMounted(() => {
// 过滤不可用的网元 // 过滤不可用的网元
neCascaderOptions.value = neInfoStore.getNeSelectOtions.filter( neCascaderOptions.value = neInfoStore.getNeSelectOtions.filter(
(item: any) => { (item: any) => {
return ['UDM'].includes(item.value); return ['AMF', 'AUSF', 'SMF', 'UDM', 'PCF'].includes(item.value);
} }
); );
if (neCascaderOptions.value.length === 0) { if (neCascaderOptions.value.length === 0) {
message.warning({ message.warning({
content: t('common.noData'), content: t('common.noData'),
duration: 2, duration: 3,
}); });
return; return;
} }
@@ -593,13 +599,11 @@ onMounted(() => {
} else { } else {
message.warning({ message.warning({
content: t('common.noData'), content: t('common.noData'),
duration: 2, duration: 3,
}); });
} }
}) })
.finally(() => { .finally(() => {
// 获取跟踪接口列表
neInfoStore.fnNeTraceInterface();
// 获取列表数据 // 获取列表数据
fnGetList(); fnGetList();
}); });
@@ -765,21 +769,40 @@ onMounted(() => {
</template> </template>
<template v-if="column.key === 'id'"> <template v-if="column.key === 'id'">
<a-space :size="8" align="center"> <a-space :size="8" align="center">
<div v-perms:has="['traceManage:task:data']">
<a-tooltip placement="topRight">
<template #title>
{{ t('views.traceManage.task.dataView') }}
</template>
<a-button
type="link"
@click.prevent="fnRecordView(record.traceId, 'data')"
>
<template #icon><ContainerOutlined /></template>
</a-button>
</a-tooltip>
</div>
<div v-perms:has="['traceManage:task:analyze']">
<a-tooltip placement="topRight">
<template #title>
{{ t('views.traceManage.task.pcapView') }}
</template>
<a-button
type="link"
@click.prevent="fnRecordView(record.traceId, 'analyze')"
>
<template #icon><BarsOutlined /></template>
</a-button>
</a-tooltip>
</div>
<a-tooltip placement="topRight"> <a-tooltip placement="topRight">
<template #title>{{ t('common.editText') }}</template> <template #title>{{ t('common.viewText') }}</template>
<a-button <a-button
type="link" type="link"
@click.prevent="fnModalOpenByEdit(record.id)" @click.prevent="fnModalOpenByEdit(record.id)"
> >
<template #icon><FormOutlined /></template> <template #icon><ProfileOutlined /></template>
</a-button>
</a-tooltip>
<a-tooltip placement="topRight">
<template #title>{{
t('views.traceManage.task.pcapView')
}}</template>
<a-button type="link" @click.prevent="fnRecordPCAPView(record)">
<template #icon><ContainerOutlined /></template>
</a-button> </a-button>
</a-tooltip> </a-tooltip>
<a-tooltip placement="topRight"> <a-tooltip placement="topRight">
@@ -810,12 +833,14 @@ onMounted(() => {
:confirm-loading="modalState.confirmLoading" :confirm-loading="modalState.confirmLoading"
@ok="fnModalOk" @ok="fnModalOk"
@cancel="fnModalCancel" @cancel="fnModalCancel"
:footer="modalState.from.id ? null : undefined"
> >
<a-form <a-form
name="modalStateFrom" name="modalStateFrom"
layout="horizontal" layout="horizontal"
:label-col="{ span: 4 }" :label-col="{ span: 4 }"
:label-wrap="true" :label-wrap="true"
:disabled="!!modalState.from.id"
> >
<a-row> <a-row>
<a-col :lg="12" :md="12" :xs="24"> <a-col :lg="12" :md="12" :xs="24">
@@ -823,12 +848,13 @@ onMounted(() => {
:label="t('views.ne.common.neType')" :label="t('views.ne.common.neType')"
:label-col="{ span: 8 }" :label-col="{ span: 8 }"
name="neType" name="neType"
v-bind="modalStateFrom.validateInfos.neId" v-bind="modalStateFrom.validateInfos.neList"
> >
<a-cascader <a-cascader
v-model:value="modalState.neType" v-model:value="modalState.neType"
:options="neCascaderOptions" :options="neCascaderOptions"
@change="fnNeChange" @change="fnNeChange"
multiple
:allow-clear="false" :allow-clear="false"
:placeholder="t('views.ne.common.neTypePlease')" :placeholder="t('views.ne.common.neTypePlease')"
/> />
@@ -844,7 +870,6 @@ onMounted(() => {
<a-select <a-select
v-model:value="modalState.from.traceType" v-model:value="modalState.from.traceType"
:options="dict.traceType" :options="dict.traceType"
@change="fnTraceTypeChange"
:allow-clear="false" :allow-clear="false"
:placeholder="t('views.traceManage.task.trackTypePlease')" :placeholder="t('views.traceManage.task.trackTypePlease')"
> >
@@ -861,11 +886,9 @@ onMounted(() => {
<a-range-picker <a-range-picker
v-model:value="modalState.timeRangePicker" v-model:value="modalState.timeRangePicker"
@change="fnRangePickerChange" @change="fnRangePickerChange"
allow-clear
bordered bordered
:show-time="{ format: 'HH:mm:ss' }" :show-time="{ format: 'HH:mm:ss' }"
format="YYYY-MM-DD HH:mm:ss" format="YYYY-MM-DD HH:mm:ss"
value-format="x"
style="width: 100%" style="width: 100%"
:disabled-date="fnRangePickerDisabledDate" :disabled-date="fnRangePickerDisabledDate"
:placeholder="[ :placeholder="[
@@ -894,8 +917,8 @@ onMounted(() => {
<a-select <a-select
mode="multiple" mode="multiple"
:placeholder="t('views.traceManage.task.interfacesPlease')" :placeholder="t('views.traceManage.task.interfacesPlease')"
v-model:value="modalState.neTypeInterfaceSelect" v-model:value="modalState.neTypeInterface"
:options="modalState.neTypeInterface" :options="dict.traceInterfaces"
@change="fnSelectInterface" @change="fnSelectInterface"
> >
</a-select> </a-select>
@@ -904,26 +927,6 @@ onMounted(() => {
<!-- 设备跟踪 --> <!-- 设备跟踪 -->
<template v-if="modalState.from.traceType === '2'"> <template v-if="modalState.from.traceType === '2'">
<a-form-item
:label="t('views.traceManage.task.signalPort')"
name="signalPort"
v-bind="modalStateFrom.validateInfos.signalPort"
>
<a-input-number
v-model:value="modalState.from.signalPort"
style="width: 100%"
:placeholder="t('views.traceManage.task.signalPortPlease')"
>
<template #prefix>
<a-tooltip placement="topLeft">
<template #title>
<div>{{ t('views.traceManage.task.signalPortTip') }}</div>
</template>
<InfoCircleOutlined style="opacity: 0.45; color: inherit" />
</a-tooltip>
</template>
</a-input-number>
</a-form-item>
<a-row> <a-row>
<a-col :lg="12" :md="12" :xs="24"> <a-col :lg="12" :md="12" :xs="24">
<a-form-item <a-form-item
@@ -1000,7 +1003,7 @@ onMounted(() => {
</template> </template>
</a-input> </a-input>
</a-form-item> </a-form-item>
<a-form-item <!-- <a-form-item
:label="t('views.traceManage.task.msisdn')" :label="t('views.traceManage.task.msisdn')"
name="msisdn" name="msisdn"
v-bind="modalStateFrom.validateInfos.msisdn" v-bind="modalStateFrom.validateInfos.msisdn"
@@ -1019,7 +1022,7 @@ onMounted(() => {
</a-tooltip> </a-tooltip>
</template> </template>
</a-input> </a-input>
</a-form-item> </a-form-item> -->
</template> </template>
</a-form> </a-form>
</ProModal> </ProModal>

View File

@@ -238,15 +238,13 @@ watchEffect(() => {
.thead-item:nth-child(3), .thead-item:nth-child(3),
.tbody-item:nth-child(3), .tbody-item:nth-child(3),
.thead-item:nth-child(4), .thead-item:nth-child(4),
.tbody-item:nth-child(4) { .tbody-item:nth-child(4),
.thead-item:nth-child(5),
.tbody-item:nth-child(5) {
flex-basis: 8rem; flex-basis: 8rem;
width: 8rem; width: 8rem;
overflow-y: auto; overflow-y: auto;
} text-wrap: nowrap;
.thead-item:nth-child(5),
.tbody-item:nth-child(5) {
flex-basis: 7rem;
width: 7rem;
} }
.thead-item:nth-child(6), .thead-item:nth-child(6),
.tbody-item:nth-child(6) { .tbody-item:nth-child(6) {
@@ -270,6 +268,7 @@ watchEffect(() => {
.tbody-item:nth-child(2)::-webkit-scrollbar, .tbody-item:nth-child(2)::-webkit-scrollbar,
.tbody-item:nth-child(3)::-webkit-scrollbar, .tbody-item:nth-child(3)::-webkit-scrollbar,
.tbody-item:nth-child(4)::-webkit-scrollbar, .tbody-item:nth-child(4)::-webkit-scrollbar,
.tbody-item:nth-child(5)::-webkit-scrollbar,
.tbody-item:nth-child(7)::-webkit-scrollbar { .tbody-item:nth-child(7)::-webkit-scrollbar {
width: 4px; /* 设置滚动条宽度 */ width: 4px; /* 设置滚动条宽度 */
height: 4px; height: 4px;
@@ -278,6 +277,7 @@ watchEffect(() => {
.tbody-item:nth-child(2)::-webkit-scrollbar-track, .tbody-item:nth-child(2)::-webkit-scrollbar-track,
.tbody-item:nth-child(3)::-webkit-scrollbar-track, .tbody-item:nth-child(3)::-webkit-scrollbar-track,
.tbody-item:nth-child(4)::-webkit-scrollbar-track, .tbody-item:nth-child(4)::-webkit-scrollbar-track,
.tbody-item:nth-child(5)::-webkit-scrollbar-track,
.tbody-item:nth-child(7)::-webkit-scrollbar-track { .tbody-item:nth-child(7)::-webkit-scrollbar-track {
background-color: #f0f0f0; /* 设置滚动条轨道背景颜色 */ background-color: #f0f0f0; /* 设置滚动条轨道背景颜色 */
} }
@@ -285,6 +285,7 @@ watchEffect(() => {
.tbody-item:nth-child(2)::-webkit-scrollbar-thumb, .tbody-item:nth-child(2)::-webkit-scrollbar-thumb,
.tbody-item:nth-child(3)::-webkit-scrollbar-thumb, .tbody-item:nth-child(3)::-webkit-scrollbar-thumb,
.tbody-item:nth-child(4)::-webkit-scrollbar-thumb, .tbody-item:nth-child(4)::-webkit-scrollbar-thumb,
.tbody-item:nth-child(5)::-webkit-scrollbar-thumb,
.tbody-item:nth-child(7)::-webkit-scrollbar-thumb { .tbody-item:nth-child(7)::-webkit-scrollbar-thumb {
background-color: #bfbfbf; /* 设置滚动条滑块颜色 */ background-color: #bfbfbf; /* 设置滚动条滑块颜色 */
} }
@@ -292,6 +293,7 @@ watchEffect(() => {
.tbody-item:nth-child(2)::-webkit-scrollbar-thumb:hover, .tbody-item:nth-child(2)::-webkit-scrollbar-thumb:hover,
.tbody-item:nth-child(3)::-webkit-scrollbar-thumb:hover, .tbody-item:nth-child(3)::-webkit-scrollbar-thumb:hover,
.tbody-item:nth-child(4)::-webkit-scrollbar-thumb:hover, .tbody-item:nth-child(4)::-webkit-scrollbar-thumb:hover,
.tbody-item:nth-child(5)::-webkit-scrollbar-thumb:hover,
.tbody-item:nth-child(7)::-webkit-scrollbar-thumb:hover { .tbody-item:nth-child(7)::-webkit-scrollbar-thumb:hover {
background-color: #1890ff; /* 设置鼠标悬停时滚动条滑块颜色 */ background-color: #1890ff; /* 设置鼠标悬停时滚动条滑块颜色 */
} }

View File

@@ -31,11 +31,11 @@ type StateType = {
/**当前选中的帧编号 */ /**当前选中的帧编号 */
selectedFrame: number; selectedFrame: number;
/**当前选中的帧数据 */ /**当前选中的帧数据 */
selectedPacket: { tree: any[]; data_sources: any[] }; packetFrame: { tree: any[]; data_sources: any[] };
/**pcap包帧数据 */ /**pcap包帧数据 */
packetFrameData: Map<string, any> | null; packetFrameTreeMap: Map<string, any> | null;
/**当前选中的帧数据-空占位 */ /**当前选中的帧数据 */
selectedTreeEntry: typeof NO_SELECTION; selectedTree: typeof NO_SELECTION;
/**选择帧的Dump数据标签 */ /**选择帧的Dump数据标签 */
selectedDataSourceIndex: number; selectedDataSourceIndex: number;
/**处理完成状态 */ /**处理完成状态 */
@@ -69,11 +69,11 @@ export function usePCAP() {
filter: '', filter: '',
filterError: null, filterError: null,
currentFilter: '', currentFilter: '',
selectedFrame: 1, selectedFrame: 0,
/**当前选中的帧数据 */ /**当前选中的帧数据 */
selectedPacket: { tree: [], data_sources: [] }, packetFrame: { tree: [], data_sources: [] },
packetFrameData: null, // 注意Map 需要额外处理 packetFrameTreeMap: null, // 注意Map 需要额外处理
selectedTreeEntry: NO_SELECTION, // NO_SELECTION 需要定义 selectedTree: NO_SELECTION, // NO_SELECTION 需要定义
/**选择帧的Dump数据标签 */ /**选择帧的Dump数据标签 */
selectedDataSourceIndex: 0, selectedDataSourceIndex: 0,
/**处理完成状态 */ /**处理完成状态 */
@@ -91,9 +91,9 @@ export function usePCAP() {
state.nextPageNum = 1; state.nextPageNum = 1;
// 选择帧的数据 // 选择帧的数据
state.selectedFrame = 0; state.selectedFrame = 0;
state.selectedPacket = { tree: [], data_sources: [] }; state.packetFrame = { tree: [], data_sources: [] };
state.packetFrameData = null; state.packetFrameTreeMap = null;
state.selectedTreeEntry = NO_SELECTION; state.selectedTree = NO_SELECTION;
state.selectedDataSourceIndex = 0; state.selectedDataSourceIndex = 0;
} }
@@ -121,23 +121,23 @@ export function usePCAP() {
} }
/**帧数据点击选中 */ /**帧数据点击选中 */
function handleSelectedTreeEntry(e: any) { function handleSelectedTree(e: any) {
console.log('fnSelectedTreeEntry', e); // console.log('fnSelectedTree', e);
state.selectedTreeEntry = e; state.selectedTree = e;
} }
/**报文数据点击选中 */ /**报文数据点击选中 */
function handleSelectedFindSelection(src_idx: number, pos: number) { function handleSelectedFindSelection(src_idx: number, pos: number) {
console.log('fnSelectedFindSelection', pos); // console.log('fnSelectedFindSelection', pos);
if (state.packetFrameData == null) return; if (state.packetFrameTreeMap == null) return;
// find the smallest one // find the smallest one
let current = null; let current = null;
for (let [k, pp] of state.packetFrameData) { for (let [k, pp] of state.packetFrameTreeMap) {
if (pp.idx !== src_idx) continue; if (pp.idx !== src_idx) continue;
if (pos >= pp.start && pos <= pp.start + pp.length) { if (pos >= pp.start && pos <= pp.start + pp.length) {
if ( if (
current != null && current != null &&
state.packetFrameData.get(current).length > pp.length state.packetFrameTreeMap.get(current).length > pp.length
) { ) {
current = k; current = k;
} else { } else {
@@ -146,19 +146,19 @@ export function usePCAP() {
} }
} }
if (current != null) { if (current != null) {
state.selectedTreeEntry = state.packetFrameData.get(current); state.selectedTree = state.packetFrameTreeMap.get(current);
} }
} }
/**包数据表点击选中 */ /**包数据表点击选中 */
function handleSelectedFrame(no: number) { function handleSelectedFrame(no: number) {
console.log('fnSelectedFrame', no, state.totalFrames); // console.log('fnSelectedFrame', no, state.totalFrames);
state.selectedFrame = no; state.selectedFrame = no;
wk.send({ type: 'select', number: state.selectedFrame }); wk.send({ type: 'select', number: state.selectedFrame });
} }
/**包数据表滚动底部加载 */ /**包数据表滚动底部加载 */
function handleScrollBottom() { function handleScrollBottom() {
const totalFetched = state.packetFrames.length; const totalFetched = state.packetFrames.length;
console.log('fnScrollBottom', totalFetched); // console.log('fnScrollBottom', totalFetched);
if (!state.nextPageLoad && totalFetched < state.totalFrames) { if (!state.nextPageLoad && totalFetched < state.totalFrames) {
state.nextPageLoad = true; state.nextPageLoad = true;
state.nextPageNum++; state.nextPageNum++;
@@ -167,7 +167,7 @@ export function usePCAP() {
} }
/**包数据表过滤 */ /**包数据表过滤 */
function handleFilterFrames() { function handleFilterFrames() {
console.log('fnFilterFinish', state.filter); // console.log('fnFilterFinish', state.filter);
wk.send({ type: 'check-filter', filter: state.filter }); wk.send({ type: 'check-filter', filter: state.filter });
} }
/**包数据表加载 */ /**包数据表加载 */
@@ -254,9 +254,9 @@ export function usePCAP() {
} }
break; break;
case 'selected': case 'selected':
state.selectedPacket = res.data; state.packetFrame = res.data;
state.packetFrameData = parseFrameData('root', res.data); state.packetFrameTreeMap = parseFrameData('root', res.data);
state.selectedTreeEntry = NO_SELECTION; state.selectedTree = NO_SELECTION;
state.selectedDataSourceIndex = 0; state.selectedDataSourceIndex = 0;
break; break;
case 'processed': case 'processed':
@@ -306,7 +306,7 @@ export function usePCAP() {
return { return {
state, state,
handleSelectedTreeEntry, handleSelectedTree,
handleSelectedFindSelection, handleSelectedFindSelection,
handleSelectedFrame, handleSelectedFrame,
handleScrollBottom, handleScrollBottom,

View File

@@ -8,11 +8,12 @@ import DissectionDump from './components/DissectionDump.vue';
import PacketTable from './components/PacketTable.vue'; import PacketTable from './components/PacketTable.vue';
import { usePCAP, NO_SELECTION } from './hooks/usePCAP'; import { usePCAP, NO_SELECTION } from './hooks/usePCAP';
import { parseSizeFromFile } from '@/utils/parse-utils'; import { parseSizeFromFile } from '@/utils/parse-utils';
import { parseDateToStr } from '@/utils/date-utils';
import useI18n from '@/hooks/useI18n'; import useI18n from '@/hooks/useI18n';
const { t } = useI18n(); const { t } = useI18n();
const { const {
state, state,
handleSelectedTreeEntry, handleSelectedTree,
handleSelectedFindSelection, handleSelectedFindSelection,
handleSelectedFrame, handleSelectedFrame,
handleScrollBottom, handleScrollBottom,
@@ -49,8 +50,9 @@ function fnUpload(up: UploadRequestOption) {
:loading="!state.initialized" :loading="!state.initialized"
:body-style="{ padding: '12px' }" :body-style="{ padding: '12px' }"
> >
<div class="toolbar"> <!-- 插槽-卡片左侧侧 -->
<a-space :size="8" class="toolbar-oper"> <template #title>
<a-space :size="8" align="center">
<a-upload <a-upload
name="file" name="file"
list-type="picture" list-type="picture"
@@ -64,61 +66,94 @@ function fnUpload(up: UploadRequestOption) {
</a-upload> </a-upload>
<a-button @click="handleLoadExample()">Example</a-button> <a-button @click="handleLoadExample()">Example</a-button>
</a-space> </a-space>
</template>
<div class="toolbar-info"> <!-- 插槽-卡片右侧 -->
<template #extra>
<a-space :size="8" align="center">
<a-tag color="green" v-show="!!state.currentFilter"> <a-tag color="green" v-show="!!state.currentFilter">
{{ state.currentFilter }} {{ state.currentFilter }}
</a-tag> </a-tag>
<span> Matched Frame: {{ state.totalFrames }} </span> <span> Matched Frame: {{ state.totalFrames }} </span>
</div> <!-- 包信息 -->
<a-popover
<!-- 包信息 --> trigger="click"
<a-popover placement="bottomLeft"
trigger="click" v-if="state.summary.filename"
placement="bottomLeft" >
v-if="state.summary.filename" <template #content>
> <div class="summary">
<template #content> <div class="summary-item">
<div class="summary"> <span>Type:</span>
<div class="summary-item"> <span>{{ state.summary.file_type }}</span>
<span>Type:</span> </div>
<span>{{ state.summary.file_type }}</span> <div class="summary-item">
<span>Size:</span>
<span>{{
parseSizeFromFile(state.summary.file_length)
}}</span>
</div>
<div class="summary-item">
<span>Encapsulation:</span>
<span>{{ state.summary.file_encap_type }}</span>
</div>
<div class="summary-item">
<span>Packets:</span>
<span>{{ state.summary.packet_count }}</span>
</div>
<div class="summary-item">
<span>Start Time:</span>
<span>
{{
parseDateToStr(
state.summary.start_time * 1000,
'YYYY-MM-DD HH:mm:ss.SSS'
)
}}
</span>
</div>
<div class="summary-item">
<span>Stop Time:</span>
<span>
{{
parseDateToStr(
state.summary.stop_time * 1000,
'YYYY-MM-DD HH:mm:ss.SSS'
)
}}
</span>
</div>
<div class="summary-item">
<span>Duration:</span>
<span>{{ Math.round(state.summary.elapsed_time) }}s</span>
</div>
</div> </div>
<div class="summary-item"> </template>
<span>Size:</span> <InfoCircleOutlined />
<span>{{ parseSizeFromFile(state.summary.file_length) }}</span> </a-popover>
</div> </a-space>
<div class="summary-item"> </template>
<span>Encapsulation:</span>
<span>{{ state.summary.file_encap_type }}</span>
</div>
<div class="summary-item">
<span>Packets:</span>
<span>{{ state.summary.packet_count }}</span>
</div>
<div class="summary-item">
<span>Duration:</span>
<span>{{ Math.round(state.summary.elapsed_time) }}s</span>
</div>
</div>
</template>
<InfoCircleOutlined />
</a-popover>
</div>
<!-- 包数据表过滤 --> <!-- 包数据表过滤 -->
<a-input-group compact> <a-input-group compact>
<a-input <a-auto-complete
v-model:value="state.filter" v-model:value="state.filter"
placeholder="display filter, example: tcp" :options="[
:allow-clear="true" { value: 'http || tcp.port == 33030 || http2' },
{ value: 'ip.src== 172.17.0.19 && ip.dst == 172.17.0.77' },
{ value: 'sip || ngap' },
]"
style="width: calc(100% - 100px)" style="width: calc(100% - 100px)"
@pressEnter="handleFilterFrames" :allow-clear="true"
> >
<template #prefix> <a-input
<FilterOutlined /> placeholder="display filter, example: tcp"
</template> @pressEnter="handleFilterFrames"
</a-input> >
<template #prefix>
<FilterOutlined />
</template>
</a-input>
</a-auto-complete>
<a-button <a-button
type="primary" type="primary"
html-type="submit" html-type="submit"
@@ -143,14 +178,18 @@ function fnUpload(up: UploadRequestOption) {
:onScrollBottom="handleScrollBottom" :onScrollBottom="handleScrollBottom"
></PacketTable> ></PacketTable>
<a-row> <a-row
:gutter="16"
style="border: 2px rgb(217 217 217) solid; border-radius: 6px"
v-show="state.selectedFrame > 0"
>
<a-col :lg="12" :md="12" :xs="24" class="tree"> <a-col :lg="12" :md="12" :xs="24" class="tree">
<!-- 帧数据 --> <!-- 帧数据 -->
<DissectionTree <DissectionTree
id="root" id="root"
:select="handleSelectedTreeEntry" :select="handleSelectedTree"
:selected="state.selectedTreeEntry" :selected="state.selectedTree"
:tree="state.selectedPacket.tree" :tree="state.packetFrame.tree"
/> />
</a-col> </a-col>
<a-col :lg="12" :md="12" :xs="24" class="dump"> <a-col :lg="12" :md="12" :xs="24" class="dump">
@@ -163,15 +202,15 @@ function fnUpload(up: UploadRequestOption) {
<a-tab-pane <a-tab-pane
:key="idx" :key="idx"
:tab="v.name" :tab="v.name"
v-for="(v, idx) in state.selectedPacket.data_sources" v-for="(v, idx) in state.packetFrame.data_sources"
style="overflow: auto" style="overflow: auto"
> >
<DissectionDump <DissectionDump
:base64="v.data" :base64="v.data"
:select="(pos:number)=>handleSelectedFindSelection(idx, pos)" :select="(pos:number)=>handleSelectedFindSelection(idx, pos)"
:selected=" :selected="
idx === state.selectedTreeEntry.idx idx === state.selectedTree.idx
? state.selectedTreeEntry ? state.selectedTree
: NO_SELECTION : NO_SELECTION
" "
/> />
@@ -184,17 +223,6 @@ function fnUpload(up: UploadRequestOption) {
</template> </template>
<style scoped> <style scoped>
.toolbar {
display: flex;
align-items: center;
margin-bottom: 12px;
}
.toolbar-info {
flex: 1;
text-align: right;
padding-right: 8px;
}
.summary { .summary {
display: flex; display: flex;
flex-direction: column; flex-direction: column;

View File

@@ -1,16 +1,16 @@
<script setup lang="ts"> <script setup lang="ts">
import { onBeforeUnmount, onMounted, reactive, ref } from 'vue'; import { onBeforeUnmount, onMounted, reactive, ref, toRaw } from 'vue';
import { PageContainer } from 'antdv-pro-layout'; import { PageContainer } from 'antdv-pro-layout';
import { message, Modal } from 'ant-design-vue/es'; import { message, Modal } from 'ant-design-vue/es';
import DissectionTree from '../tshark/components/DissectionTree.vue'; import DissectionTree from '../tshark/components/DissectionTree.vue';
import DissectionDump from '../tshark/components/DissectionDump.vue'; import DissectionDump from '../tshark/components/DissectionDump.vue';
import PacketTable from '../tshark/components/PacketTable.vue';
import { import {
RESULT_CODE_ERROR, RESULT_CODE_ERROR,
RESULT_CODE_SUCCESS, RESULT_CODE_SUCCESS,
} from '@/constants/result-constants'; } from '@/constants/result-constants';
import { filePullTask } from '@/api/trace/task'; import { scriptUrl as wkUrl } from '@/assets/js/wiregasm_worker';
import { OptionsType, WS } from '@/plugins/ws-websocket'; import * as wkUtil from '@/plugins/wk-worker';
import * as wsUtil from '@/plugins/ws-websocket';
import useI18n from '@/hooks/useI18n'; import useI18n from '@/hooks/useI18n';
import saveAs from 'file-saver'; import saveAs from 'file-saver';
import { import {
@@ -19,34 +19,20 @@ import {
packetStop, packetStop,
packetFilter, packetFilter,
packetKeep, packetKeep,
packetPCAPFile,
} from '@/api/trace/packet'; } from '@/api/trace/packet';
const ws = new WS(); import { parseDateToStr } from '@/utils/date-utils';
import { ColumnsType } from 'ant-design-vue/es/table';
const ws = new wsUtil.WS();
const wk = new wkUtil.WK();
const { t } = useI18n(); const { t } = useI18n();
// =========== WK ==============
const NO_SELECTION = { id: '', idx: 0, start: 0, length: 0 }; const NO_SELECTION = { id: '', idx: 0, start: 0, length: 0 };
type StateType = { type StateType = {
/**网卡设备列表 */
devices: { id: string; label: string; children: any[] }[];
/**初始化 */ /**初始化 */
initialized: boolean; initialized: boolean;
/**保活调度器 */
keepTimer: any;
/**任务 */
task: {
taskNo: string;
device: string;
filter: string;
outputPCAP: boolean;
};
/**字段 */
columns: string[];
/**过滤条件 */
filter: string;
/**过滤条件错误信息 */
filterError: string | null;
/**当前选中的帧编号 */ /**当前选中的帧编号 */
selectedFrame: number; selectedFrame: number;
/**当前选中的帧数据 */ /**当前选中的帧数据 */
@@ -57,63 +43,19 @@ type StateType = {
selectedTree: typeof NO_SELECTION; selectedTree: typeof NO_SELECTION;
/**选择帧的Dump数据标签 */ /**选择帧的Dump数据标签 */
selectedDataSourceIndex: number; selectedDataSourceIndex: number;
/**包总数 */
totalPackets: number;
/**包数据 */
packetList: any[];
}; };
const state = reactive<StateType>({ const state = reactive<StateType>({
devices: [],
initialized: false, initialized: false,
keepTimer: null, selectedFrame: 0,
task: {
taskNo: 'laYlTbq',
device: '192.168.5.58',
filter: 'tcp and (port 33030 or 8080)',
outputPCAP: false,
},
columns: [
'No.',
'Time',
'Source',
'Destination',
'Protocol',
'Length',
'Info',
],
filter: 'tcp and (port 33030 or 8080)',
filterError: null,
selectedFrame: 1,
/**当前选中的帧数据 */ /**当前选中的帧数据 */
packetFrame: { tree: [], data_sources: [] }, packetFrame: { tree: [], data_sources: [] },
packetFrameTreeMap: null, // 注意Map 需要额外处理 packetFrameTreeMap: null, // 注意Map 需要额外处理
selectedTree: NO_SELECTION, // NO_SELECTION 需要定义 selectedTree: NO_SELECTION, // NO_SELECTION 需要定义
/**选择帧的Dump数据标签 */ /**选择帧的Dump数据标签 */
selectedDataSourceIndex: 0, selectedDataSourceIndex: 0,
// 包数据
totalPackets: 0,
packetList: [],
}); });
/**清除帧数据和报文信息状态 */
function fnReset() {
state.initialized = false;
// 选择帧的数据
state.selectedFrame = 0;
state.packetFrame = { tree: [], data_sources: [] };
state.packetFrameTreeMap = null;
state.selectedTree = NO_SELECTION;
state.selectedDataSourceIndex = 0;
// 过滤条件
state.filter = 'tcp and (port 33030 or 8080)';
state.filterError = null;
// 包数据
state.totalPackets = 0;
state.packetList = [];
}
/**解析帧数据为简单结构 */ /**解析帧数据为简单结构 */
function parseFrameTree(id: string, node: Record<string, any>) { function parseFrameTree(id: string, node: Record<string, any>) {
let map = new Map(); let map = new Map();
@@ -136,15 +78,9 @@ function parseFrameTree(id: string, node: Record<string, any>) {
return map; return map;
} }
/**帧数据点击选中 */
function handleSelectedTreeEntry(e: any) {
console.log('fnSelectedTreeEntry', e);
state.selectedTree = e;
}
/**报文数据点击选中 */ /**报文数据点击选中 */
function handleSelectedFindSelection(src_idx: number, pos: number) { function handleSelectedFindSelection(src_idx: number, pos: number) {
console.log('fnSelectedFindSelection', pos); // console.log('fnSelectedFindSelection', pos);
if (state.packetFrameTreeMap == null) return; if (state.packetFrameTreeMap == null) return;
// find the smallest one // find the smallest one
let current = null; let current = null;
@@ -166,83 +102,274 @@ function handleSelectedFindSelection(src_idx: number, pos: number) {
state.selectedTree = state.packetFrameTreeMap.get(current); state.selectedTree = state.packetFrameTreeMap.get(current);
} }
} }
/**帧数据点击选中 */
function handleSelectedTree(e: any) {
state.selectedTree = e;
}
/**包数据表点击选中 */ /**接收数据后回调 */
function handleSelectedFrame(num: number) { function wkMessage(res: Record<string, any>) {
console.log('fnSelectedFrame', num, state.totalPackets); // console.log('wkMessage', res);
const packet = state.packetList.find((v: any) => v.number === num); switch (res.type) {
if (!packet) return; case 'status':
const packetFrame = packet.frame; console.info(res.status);
state.selectedFrame = packet.number; break;
state.packetFrame = packetFrame; case 'error':
state.packetFrameTreeMap = parseFrameTree('root', packetFrame); console.warn(res.error);
state.selectedTree = NO_SELECTION; break;
state.selectedDataSourceIndex = 0; case 'init':
wk.send({ type: 'columns' });
state.initialized = true;
break;
case 'frames':
const { frames } = res.data;
// 有匹配的选择第一个
if (frames.length > 0) {
state.selectedFrame = frames[0].number;
wk.send({ type: 'select', number: state.selectedFrame });
}
break;
case 'selected':
// 首行修改帧编号
const fristFrame = res.data.tree[0];
res.data.tree[0].label = fristFrame.label.replace(
'Frame 1:',
`Frame ${tableState.selectedNumber}:`
);
const item = fristFrame.tree.find(
(item: any) => item.label === 'Frame Number: 1'
);
if (item) {
item.label = `Frame Number: ${tableState.selectedNumber}:`;
}
state.packetFrame = res.data;
state.packetFrameTreeMap = parseFrameTree('root', res.data);
state.selectedTree = NO_SELECTION;
state.selectedDataSourceIndex = 0;
break;
case 'processed':
// setStatus(`Error: non-zero return code (${e.data.code})`);
// 加载数据
wk.send({
type: 'frames',
filter: '',
skip: 0,
limit: 1,
});
break;
default:
console.warn(res);
break;
}
} }
/**包数据表滚动底部加载 */ /**建立WK连接 */
function handleScrollBottom(index: any) { function fnWK() {
console.log('handleScrollBottom', index); const options: wkUtil.OptionsType = {
url: wkUrl,
onmessage: wkMessage,
onerror: (ev: any) => {
console.error(ev);
},
};
//建立连接
wk.connect(options);
} }
// =========== WS ==============
/**接收数据后回调 */
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) {
tableState.data = [];
tableState.total = 0;
taskState.keepTimer = setInterval(() => {
if (ws.state() != WebSocket.OPEN) {
clearInterval(taskState.keepTimer);
return;
}
packetKeep(taskState.task.taskNo, 120);
}, 90 * 1000);
return;
}
// 订阅组信息
if (!data?.groupId) {
return;
}
if (data.groupId === `4_${taskState.task.taskNo}`) {
const packetData = data.data;
tableState.total = packetData.number;
tableState.data.unshift(packetData);
if (tableState.data.length > 100) {
tableState.data.pop();
}
}
}
/**建立WS连接 */
function fnWS() {
const options: wsUtil.OptionsType = {
url: '/ws',
params: {
/**订阅通道组
*
* 信令跟踪Packet (GroupID:4_taskNo)
*/
subGroupID: `4_${taskState.task.taskNo}`,
},
heartTimer: 30 * 1000,
onmessage: wsMessage,
onerror: (ev: any) => {
// 接收数据后回调
console.error(ev);
},
};
//建立连接
ws.connect(options);
}
// =========== 任务 ==============
type TaskStateType = {
/**网卡设备列表 */
devices: { id: string; label: string; children: any[] }[];
/**任务 */
task: {
taskNo: string;
device: string;
filter: string;
outputPCAP: boolean;
};
/**过滤条件 */
filter: string;
/**过滤条件错误信息 */
filterError: string | null;
/**保活调度器 */
keepTimer: any;
/**停止标记 */
stop: boolean;
stopOutputPCAP: boolean;
};
const taskState = reactive<TaskStateType>({
devices: [],
task: {
taskNo: '',
device: '192.168.5.58',
filter: '',
outputPCAP: false,
},
filter: '',
filterError: null,
keepTimer: null,
stop: false,
stopOutputPCAP: false,
});
/**开始跟踪 */ /**开始跟踪 */
function fnStart() { function fnStart() {
// state.task.taskNo = 'laYlTbq'; if (taskState.keepTimer) return;
state.task.taskNo = Number(Date.now()).toString(16); taskState.keepTimer = true;
state.task.outputPCAP = false; const hide = message.loading(t('common.loading'), 0);
packetStart(state.task).then(res => { // taskState.task.taskNo = 'laYlTbq';
if (res.code === RESULT_CODE_SUCCESS) { taskState.task.taskNo = Number(Date.now()).toString(16);
fnReset(); taskState.task.filter = taskState.filter;
fnWS(); packetStart(toRaw(taskState.task))
} else { .then(res => {
message.error(t('common.operateErr'), 3); if (res.code === RESULT_CODE_SUCCESS) {
} // 清空选择帧的数据
}); state.selectedFrame = 0;
state.packetFrame = { tree: [], data_sources: [] };
state.packetFrameTreeMap = null;
state.selectedTree = NO_SELECTION;
state.selectedDataSourceIndex = 0;
// 开启
if (!state.initialized) {
fnWK();
}
clearInterval(taskState.keepTimer);
taskState.keepTimer = null;
fnWS();
// 记录状态
taskState.stop = false;
taskState.stopOutputPCAP = taskState.task.outputPCAP;
} else {
message.error(t('common.operateErr'), 3);
}
})
.finally(() => {
hide();
});
} }
/**停止跟踪 */ /**停止跟踪 */
function fnStop() { function fnStop() {
packetStop(state.task.taskNo).then(res => { if (typeof taskState.keepTimer !== 'number') return;
if (res.code === RESULT_CODE_SUCCESS) { const hide = message.loading(t('common.loading'), 0);
packetStop(taskState.task.taskNo)
.then(res => {
if (res.code !== RESULT_CODE_SUCCESS) {
message.warning(res.msg, 3);
}
// 关闭监听
ws.close(); ws.close();
state.initialized = false; clearInterval(taskState.keepTimer);
state.filter = ''; taskState.keepTimer = null;
state.filterError = null;
} else { taskState.filter = '';
message.warning(res.msg, 3); taskState.filterError = null;
}
}); taskState.stop = true;
})
.finally(() => {
hide();
});
} }
/**跟踪数据表过滤 */ /**跟踪数据表过滤 */
function handleFilterFrames() { function handleFilterFrames() {
packetFilter(state.task.taskNo, state.filter).then(res => { packetFilter(taskState.task.taskNo, taskState.filter).then(res => {
if (res.code === RESULT_CODE_SUCCESS) { if (res.code === RESULT_CODE_SUCCESS) {
state.task.filter = state.filter; taskState.task.filter = taskState.filter;
taskState.filterError = null;
} else { } else {
state.filterError = res.msg; taskState.filterError = res.msg;
} }
}); });
} }
/**开始跟踪 */ /**选择网卡 */
function fnDevice(v: string) { function fnDevice(v: string) {
state.task.device = v; taskState.task.device = v;
} }
/**下载触发等待 */ /**下载触发等待 */
let downLoading = ref<boolean>(false); let downLoading = ref<boolean>(false);
/**信息文件下载 */ /**信息文件下载 */
function fnDownloadPCAP() { function fnDownloadPCAP() {
if (downLoading.value) return; if (downLoading.value) return;
const fileName = `trace_packet_${state.task.taskNo}.pcap`; const fileName = `packet_${taskState.task.taskNo}.pcap`;
Modal.confirm({ Modal.confirm({
title: t('common.tipTitle'), title: t('common.tipTitle'),
content: t('views.logManage.neFile.downTip', { fileName }), content: t('views.logManage.neFile.downTip', { fileName }),
onOk() { onOk() {
downLoading.value = true; downLoading.value = true;
const hide = message.loading(t('common.loading'), 0); const hide = message.loading(t('common.loading'), 0);
filePullTask(state.task.taskNo) packetPCAPFile(taskState.task.taskNo)
.then(res => { .then(res => {
if (res.code === RESULT_CODE_SUCCESS) { if (res.code === RESULT_CODE_SUCCESS) {
message.success({ message.success({
@@ -267,90 +394,203 @@ function fnDownloadPCAP() {
}); });
} }
/**接收数据后回调 */ // =========== 表格数据 ==============
function wsMessage(res: Record<string, any>) { /**表格状态类型 */
const { code, requestId, data } = res; type TabeStateType = {
if (code === RESULT_CODE_ERROR) { /**加载等待 */
console.warn(res.msg); loading: boolean;
return; /**记录数据 */
} data: Record<string, any>[];
/**总记录数 */
total: number;
/**选择帧编号 */
selectedNumber: number;
};
// 建联时发送请求 /**表格状态 */
if (!requestId && data.clientId) { let tableState: TabeStateType = reactive({
state.initialized = true; loading: false,
state.keepTimer = setInterval(() => { data: [],
packetKeep(state.task.taskNo, 120); total: 0,
}, 90 * 1000); selectedNumber: 0,
return; });
}
// 订阅组信息 /**表格字段列 */
if (!data?.groupId) { let tableColumns = ref<ColumnsType>([
{
title: 'Number',
dataIndex: 'number',
align: 'left',
width: 100,
},
{
title: 'Time',
dataIndex: 'time',
align: 'left',
width: 150,
customRender(opt) {
if (!opt.value) return '';
const nanoseconds = opt.value; // 纳秒时间戳
const milliseconds = Math.floor(nanoseconds / 1000000);
const nanosecondsPart = (nanoseconds % 1000000)
.toString()
.padStart(6, '0');
return `${parseDateToStr(
milliseconds,
'HH:mm:ss.SSS'
)}.${nanosecondsPart}`;
},
},
{
title: 'Source',
dataIndex: 'source',
align: 'left',
width: 150,
ellipsis: true,
},
{
title: 'Destination',
dataIndex: 'destination',
align: 'left',
width: 150,
ellipsis: true,
},
{
title: 'Protocol',
dataIndex: 'protocol',
key: 'protocol',
align: 'left',
width: 100,
},
{
title: 'length',
dataIndex: 'length',
align: 'right',
width: 100,
},
{
title: 'Info',
dataIndex: 'info',
align: 'left',
ellipsis: true,
},
]);
/**
* 查看帧数据
* @param row 记录信息
*/
function fnVisible(row: Record<string, any>) {
// 选中行重复点击时显示隐藏
if (row.id === tableState.selectedNumber && state.selectedFrame !== 0) {
state.selectedFrame = 0;
return; return;
} }
if (data.groupId === `4_${state.task.taskNo}`) { tableState.selectedNumber = row.number;
const packetData = data.data; const blob = generatePCAP(row.time / 1e3, row.data);
state.totalPackets = packetData.number; wk.send({ type: 'process', file: blob });
state.packetList.push(packetData);
}
} }
/**生成PCAP-blob */
function generatePCAP(timestamp: number, base64Data: string): Blob {
// 1. 转换数据
const binaryString = atob(base64Data);
const len = binaryString.length;
const bytes = new Uint8Array(len);
for (let i = 0; i < len; i++) {
bytes[i] = binaryString.charCodeAt(i);
}
/**建立WS连接 */ // 2. 创建PCAP文件头
function fnWS() { const fileHeader = new Uint8Array([
const options: OptionsType = { 0xd4,
url: '/ws', 0xc3,
params: { 0xb2,
/**订阅通道组 0xa1, // magic_number (微秒级)
* 0x02,
* 信令跟踪Packet (GroupID:4_taskNo) 0x00, // version_major (2)
*/ 0x04,
subGroupID: `4_${state.task.taskNo}`, 0x00, // version_minor (4)
}, 0x00,
onmessage: wsMessage, 0x00,
onerror: (ev: any) => { 0x00,
// 接收数据后回调 0x00, // thiszone (UTC)
console.error(ev); 0x00,
}, 0x00,
}; 0x00,
//建立连接 0x00, // sigfigs (固定0)
ws.connect(options); 0xff,
0xff,
0x00,
0x00, // snaplen (最大捕获长度)
0x01,
0x00,
0x00,
0x00, // network (LINKTYPE_ETHERNET)
]);
// 3. 生成时间戳
const date = new Date(timestamp);
const secs = Math.floor(date.getTime() / 1000);
const usecs = (date.getTime() % 1000) * 1000;
// 4. 构造PCAP报文头
const packetHeader = new Uint8Array(16);
const headerView = new DataView(packetHeader.buffer);
headerView.setUint32(0, secs, true); // 时间戳秒
headerView.setUint32(4, usecs, true); // 时间戳微秒
headerView.setUint32(8, bytes.length, true); // 捕获长度
headerView.setUint32(12, bytes.length, true); // 原始长度
// 5. 合并所有数据
const finalData = new Uint8Array(
fileHeader.length + packetHeader.length + bytes.length
);
finalData.set(fileHeader);
finalData.set(packetHeader, fileHeader.length);
finalData.set(bytes, fileHeader.length + packetHeader.length);
// 6. 文件Blob对象
return new Blob([finalData], { type: 'application/octet-stream' });
} }
onMounted(() => { onMounted(() => {
packetDevices().then(res => { packetDevices().then(res => {
if (res.code === RESULT_CODE_SUCCESS) { if (res.code === RESULT_CODE_SUCCESS) {
state.devices = res.data; taskState.devices = res.data;
if (res.data.length === 0) return; if (res.data.length === 0) return;
state.task.device = res.data[0].id; taskState.task.device = res.data[0].id;
} }
}); });
fnWK();
}); });
onBeforeUnmount(() => { onBeforeUnmount(() => {
clearInterval(state.keepTimer); clearInterval(taskState.keepTimer);
state.keepTimer = null; wk.send({ type: 'close' }) && wk.close();
if (ws.state() === WebSocket.OPEN) ws.close(); if (ws.state() <= WebSocket.OPEN) ws.close();
}); });
</script> </script>
<template> <template>
<PageContainer> <PageContainer>
<a-card :bordered="false" :body-style="{ padding: '12px' }"> <a-card :bordered="false" :body-style="{ padding: '12px' }">
<div class="toolbar"> <!-- 插槽-卡片左侧侧 -->
<a-space :size="8" class="toolbar-oper"> <template #title>
<a-space :size="8" align="end">
<a-dropdown-button <a-dropdown-button
type="primary" type="primary"
:disabled="state.initialized"
@click="fnStart" @click="fnStart"
:disabled="!taskState.stop && taskState.task.taskNo !== ''"
> >
<PlayCircleOutlined /> <PlayCircleOutlined />
Start Trace Start Trace
<template #overlay> <template #overlay>
<a-menu <a-menu
@click="({ key }:any) => fnDevice(key)" @click="({ key }:any) => fnDevice(key)"
:selectedKeys="[state.task.device]" :selectedKeys="[taskState.task.device]"
> >
<a-menu-item v-for="v in state.devices" :key="v.id"> <a-menu-item v-for="v in taskState.devices" :key="v.id">
<a-popover placement="rightTop" trigger="hover"> <a-popover placement="rightTop" trigger="hover">
<template #content> <template #content>
<div v-for="c in v.children">{{ c.id }}</div> <div v-for="c in v.children">{{ c.id }}</div>
@@ -366,79 +606,130 @@ onBeforeUnmount(() => {
<template #icon><DownOutlined /></template> <template #icon><DownOutlined /></template>
</a-dropdown-button> </a-dropdown-button>
<a-button danger @click.prevent="fnStop()" v-if="state.initialized"> <a-button
danger
@click.prevent="fnStop()"
:disabled="taskState.stop || taskState.task.taskNo === ''"
>
<template #icon><CloseCircleOutlined /></template> <template #icon><CloseCircleOutlined /></template>
Stop Trace Stop Trace
</a-button> </a-button>
<a-checkbox
v-model:checked="taskState.task.outputPCAP"
:disabled="!taskState.stop && taskState.task.taskNo !== ''"
>
Output PCAP
</a-checkbox>
<a-tag color="processing" v-if="taskState.task.filter !== ''">
{{ taskState.task.filter }}
</a-tag>
</a-space>
</template>
<!-- 插槽-卡片右侧 -->
<template #extra>
<a-space :size="8" align="end">
<a-button <a-button
type="primary" type="primary"
:loading="downLoading" :loading="downLoading"
@click.prevent="fnDownloadPCAP()" @click.prevent="fnDownloadPCAP()"
v-if="state.task.outputPCAP" v-if="taskState.stop && taskState.stopOutputPCAP"
> >
<template #icon><DownloadOutlined /></template> <template #icon><DownloadOutlined /></template>
{{ t('common.downloadText') }} {{ t('common.downloadText') }}
</a-button> </a-button>
<a-tag
color="green"
v-show="!!state.task.filter && state.initialized"
>
{{ state.task.filter }}
</a-tag>
</a-space>
<a-space :size="8" class="toolbar-info" v-show="state.initialized">
<span> <span>
{{ t('views.traceManage.task.traceId') }}:&nbsp; Packets:
<strong>{{ state.task.taskNo }}</strong> <strong>{{ tableState.total }}</strong>
</span>
<span>
Task No:
<strong>{{ taskState.task.taskNo }}</strong>
</span> </span>
<span> Packets: {{ state.totalPackets }} </span>
</a-space> </a-space>
</div> </template>
<!-- 包数据表过滤 --> <!-- 包数据表过滤 -->
<a-input-group compact v-show="state.initialized"> <a-input-group compact>
<a-input <a-auto-complete
v-model:value="state.filter" v-model:value="taskState.filter"
placeholder="display filter, example: tcp" :options="[
:allow-clear="true" { value: 'tcp and port 33030 and greater 100' },
{
value:
'(src 192.168.5.58 and dst port 33030) or (src 192.168.9.59 and dst port 33030)',
},
{ value: 'src host 192.168.5.58 and dst host 192.168.5.58' },
{ value: 'host 192.168.5.58 and greater 100 and less 2500' },
]"
style="width: calc(100% - 100px)" style="width: calc(100% - 100px)"
@pressEnter="handleFilterFrames" :allow-clear="true"
> >
<template #prefix> <a-input
<FilterOutlined /> placeholder="BPF Basic Filter, example: tcp"
</template> @pressEnter="handleFilterFrames"
</a-input> >
<template #prefix>
<FilterOutlined />
</template>
</a-input>
</a-auto-complete>
<a-button <a-button
type="primary" type="primary"
html-type="submit" html-type="submit"
style="width: 100px" style="width: 100px"
@click="handleFilterFrames" @click="handleFilterFrames"
:disabled="taskState.task.taskNo === '' || taskState.stop"
> >
Filter Filter
</a-button> </a-button>
</a-input-group> </a-input-group>
<a-alert <a-alert
:message="state.filterError" :message="taskState.filterError"
type="error" type="error"
v-if="state.filterError != null" v-if="taskState.filterError != null"
/> />
<!-- 包数据表 --> <!-- 包数据表 -->
<PacketTable <a-table
:columns="state.columns" class="table"
:data="state.packetList" row-key="number"
:selectedFrame="state.selectedFrame" :columns="tableColumns"
:onSelectedFrame="handleSelectedFrame" :loading="tableState.loading"
:onScrollBottom="handleScrollBottom" :data-source="tableState.data"
></PacketTable> size="small"
:pagination="false"
<a-row> :row-class-name="(record:any) => {
return `table-striped-${record.protocol}`
}"
:customRow="
record => {
return {
onClick: () => fnVisible(record),
};
}
"
:scroll="{ x: tableColumns.length * 150, y: '400px' }"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'protocol'">
<!-- <DictTag :options="dict.traceMsgType" :value="record.msgType" /> -->
</template>
</template>
</a-table>
<!-- 帧数据 -->
<a-row
:gutter="16"
style="border: 2px rgb(217 217 217) solid; border-radius: 6px"
v-show="state.selectedFrame == 1"
>
<a-col :lg="12" :md="12" :xs="24" class="tree"> <a-col :lg="12" :md="12" :xs="24" class="tree">
<!-- 帧数据 --> <!-- 帧数据 -->
<DissectionTree <DissectionTree
id="root" id="root"
:select="handleSelectedTreeEntry" :select="handleSelectedTree"
:selected="state.selectedTree" :selected="state.selectedTree"
:tree="state.packetFrame.tree" :tree="state.packetFrame.tree"
/> />
@@ -474,28 +765,6 @@ onBeforeUnmount(() => {
</template> </template>
<style scoped> <style scoped>
.toolbar {
display: flex;
align-items: center;
margin-bottom: 12px;
}
.toolbar-oper {
flex: 1;
}
.toolbar-info {
text-align: right;
padding-right: 8px;
}
.summary {
display: flex;
flex-direction: column;
}
.summary-item > span:first-child {
font-weight: 600;
margin-right: 6px;
}
.tree { .tree {
font-size: 0.8125rem; font-size: 0.8125rem;
line-height: 1.5rem; line-height: 1.5rem;