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

@@ -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';
// 登录方法
/**
*
* @param data
* @returns
*/
export function login(data: Record<string, string>) {
return request({
url: '/login',
url: '/auth/login',
method: 'POST',
data: data,
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>) {
return request({
url: '/register',
url: '/auth/register',
method: 'POST',
data: data,
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
*/
export function logout() {
export const getRouter = () => {
return request({
url: '/logout',
method: 'POST',
repeatSubmit: false,
url: '/router',
method: 'GET',
});
}
};
/**
*

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 鉴权对象

View File

@@ -4,19 +4,6 @@ import { parseObjLineToHump } from '@/utils/parse-utils';
import { parseDateToStr } from '@/utils/date-utils';
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 鉴权对象

View File

@@ -1,65 +1,5 @@
import { RESULT_CODE_SUCCESS } from '@/constants/result-constants';
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,
});
}
/**
* 更新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,
});
}
/**
* 更新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>) {
return request({
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 },
});
}
/**
* 用户强制重置密码
* @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

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 },
});
}
/**
* 信令跟踪文件
* @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',
params: { traceId },
responseType: 'blob',
timeout: 60_000,
timeout: 180_000,
});
}
/**
* 获取网元跟踪接口列表
* 跟踪任务数据列表
* @param query 查询参数
* @returns object
*/
export async function getNeTraceInterfaceAll() {
// 发起请求
const result = await request({
url: `/api/rest/databaseManagement/v1/elementType/omc_db/objectType/ne_info`,
export async function listTraceData(query: Record<string, any>) {
return request({
url: '/trace/data/list',
method: 'GET',
params: query,
});
}
/**
* 查询跟踪任务数据信息
* @param id ID
* @returns object
*/
export async function getTraceData(id: string | number) {
return request({
url: `/trace/data/${id}`,
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加密数据 */
export const RESULT_CODE_ENCRYPT = 2;
export const RESULT_CODE_ENCRYPT = 200999;
/**响应-msg加密数据 */
export const RESULT_MSG_ENCRYPT: Record<string, string> = {
zh_CN: '加密!',
en_US: 'encrypt!',
en_US: 'Encrypt!',
};
/**响应-code正常成功 */
export const RESULT_CODE_SUCCESS = 1;
export const RESULT_CODE_SUCCESS = 200001;
/**响应-msg正常成功 */
export const RESULT_MSG_SUCCESS: Record<string, string> = {
@@ -17,7 +17,16 @@ export const RESULT_MSG_SUCCESS: Record<string, string> = {
};
/**响应-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错误失败 */
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!',
};
/**响应-服务器连接出错 */
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> = {
zh_CN: '数据正在处理,请勿重复提交!',

View File

@@ -2,10 +2,13 @@
export const TOKEN_RESPONSE_FIELD = 'accessToken';
/**令牌-请求头标识前缀 */
export const TOKEN_KEY_PREFIX = 'Bearer ';
export const TOKEN_KEY_PREFIX = 'Bearer';
/**令牌-请求头标识 */
export const TOKEN_KEY = 'Authorization';
/**令牌-存放Cookie标识 */
export const TOKEN_COOKIE = 'AuthOMC';
/**令牌-访问令牌存放Cookie标识 */
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!',
tablePaginationTotal: 'Total {total} items',
noData: "No Data",
zebra:'Tabular zebra pattern',
ok: 'Ok',
cancel: 'Cancel',
close: 'Close',
@@ -131,7 +130,7 @@ export default {
},
LockScreen: {
inputPlacePwd:'Lock Screen Password',
validSucc:'Validation Passed',
enter:'Enter',
validError:'Validation Failure',
backLogin:'Logout to Relogin',
backReload:'Restarting now, please wait...',
@@ -139,6 +138,14 @@ export default {
systemReset:'Resetting now, please wait...',
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.',
userNamePlease: 'Please enter the correct 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',
passwordHit: '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",
oldPasswordPleace: "Please enter the old 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",
confirmPassword: "Confirm new password",
confirmPasswordPleace: "Please confirm the new password",
@@ -506,7 +513,7 @@ export default {
delTip: 'Confirm deletion of network element information data items?',
oam: {
title: 'OAM Configuration',
sync: 'Sync to NE',
restart: 'Restart NE',
oamEnable: 'Service',
oamPort: 'Port',
snmpEnable: 'Service',
@@ -659,6 +666,19 @@ export default {
name: "Name",
downTip: 'Confirmed to download the backup file [{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: {
reloadPara5G: 'Reload',
@@ -711,8 +731,45 @@ export default {
},
neData: {
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',
},
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: {
list: "List",
topology: "Topology",
@@ -735,6 +792,12 @@ export default {
exportTip: "Confirm exporting xlsx table files based on search criteria?",
importDataEmpty: "Imported data is empty",
},
backupData: {
auth: "UDM Authentication",
sub: "UDM Subscribers",
voip: "VoIP Authentication",
volte: "IMS Subscribers",
}
},
neUser: {
auth: {
@@ -1004,25 +1067,6 @@ export default {
},
},
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: {
capArgPlease: 'Please enter tcpdump -i any support parameter',
cmd: 'Command',
@@ -1073,30 +1117,35 @@ export default {
imsiTip: 'Mobile communication IMSI number',
srcIp: 'Source IP Address',
srcIpPlease: 'Please enter the IP address',
srcIpTip: 'Current sender IPv4 address',
srcIpTip: 'sending IPv4 address',
dstIp: 'Destination 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',
interfacesPlease: 'Please enter the signaling interface',
signalPort: 'Signal Port',
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',
rangePicker: 'Task Time',
rangePickerPlease: 'Please select the start and end time of the task',
remark: 'Remark',
remarkPlease: 'Task description can be entered',
addTask: 'Add Task',
editTask: 'Modify Task',
viewTask: 'View Task',
errorTaskInfo: 'Failed to obtain task information',
delTaskTip: 'Are you sure to delete the data item with record ID {id} ?',
stopTask: 'Successful cessation of tasks {id}',
stopTaskTip: 'Confirm stopping the task with record ID {id} ?',
pcapView: "Tracking Data Analysis",
traceFile: "Tracking File",
pcapView: "Track Data Analysis",
traceFile: "Track File",
errMsg: "Error Message",
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: {
@@ -1141,13 +1190,11 @@ export default {
delSuss:'Clear successfully',
delSure:'Whether to clear this alarm',
showSet:'Show filter settings',
exportTip: "Confirm exporting xlsx table files based on search criteria?",
exportSure:'Confirm whether to export all active alarm information',
viewIdInfo:'View {alarmId} record information',
closeModal:'Close',
},
historyAlarm:{
exportSure:'Confirm whether to export all historical alarm information',
},
faultSetting:{
interfaceType:'Type',
email:'Email',
@@ -1216,12 +1263,17 @@ export default {
tailLines: 'End Lines',
},
exportFile:{
fileName:'File Source',
fileSource:'File Source',
fileSourcePlease:'Please select the source of the document',
downTip: "Confirm the download file name is [{fileName}] File?",
downTipErr: "Failed to get file",
deleteTip: "Confirm the delete file name is [{fileName}] File?",
deleteTipErr: "Failed to delete file",
selectTip:"Please select File Name",
operateLog:'Operation Log',
cdrIMS:'Voice CDR',
cdrSMF:'Data CDR',
cdrSMSC:'SMS CDR',
cdrSGWC:'Roaming Data CDR',
}
},
monitor: {
@@ -1599,7 +1651,7 @@ export default {
loginTime: 'Login Time',
status: 'Status',
userNameTip:'The account number can only contain strings of uppercase letters, lowercase letters and numbers with a minimum length of 6 digits',
passwdTip:'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',
emailTip:'Please enter the correct email address',
phoneTip:'Please enter the correct phone number',

View File

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

View File

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

View File

@@ -1,25 +1,53 @@
import Cookies from 'js-cookie';
import { TOKEN_COOKIE } from '@/constants/token-constants';
import { localRemove, localSet } from '@/utils/cache-local-utils';
import {
CACHE_LOCAL_LOCK_PASSWD,
CACHE_LOCAL_MASK,
} from '@/constants/cache-keys-constants';
import {
TOKEN_ACCESS_COOKIE,
TOKEN_REFRESH_COOKIE,
} from '@/constants/token-constants';
/**获取cookis中Token字符串 */
export function getToken(): string {
return Cookies.get(TOKEN_COOKIE) || '';
/**获取访问令牌 */
export function getAccessToken(): string {
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');
}
/**移除cookis中Token字符串,localStorage中锁屏字符串 */
export function removeToken(): void {
Cookies.remove(TOKEN_COOKIE);
/**移除访问令牌 */
export function delAccessToken(): void {
Cookies.remove(TOKEN_ACCESS_COOKIE);
localRemove(CACHE_LOCAL_MASK);
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 {
sessionGet,
sessionGetJSON,
sessionSetJSON,
} from '@/utils/cache-session-utils';
import { localGet } from '@/utils/cache-local-utils';
import { TOKEN_KEY, TOKEN_KEY_PREFIX } from '@/constants/token-constants';
import {
CACHE_LOCAL_I18N,
@@ -18,6 +24,7 @@ import {
import {
RESULT_CODE_ENCRYPT,
RESULT_CODE_ERROR,
RESULT_CODE_EXCEPTION,
RESULT_CODE_SUCCESS,
RESULT_MSG_ENCRYPT,
RESULT_MSG_ERROR,
@@ -25,10 +32,11 @@ import {
RESULT_MSG_SERVER_ERROR,
RESULT_MSG_SUCCESS,
RESULT_MSG_TIMEOUT,
RESULT_MSG_URL_NOTFOUND,
RESULT_MSG_URL_RESUBMIT,
} from '@/constants/result-constants';
import { decryptAES, encryptAES } from '@/utils/encrypt-utils';
import { localGet } from '@/utils/cache-local-utils';
import { refreshToken } from '@/api/auth';
/**响应结果类型 */
export type ResultType = {
@@ -61,7 +69,7 @@ type OptionsType = {
/**请求地址 */
url: string;
/**请求方法 */
method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';
method: 'GET' | 'POST' | 'PUT' | 'DELETE';
/**请求头 */
headers?: HeadersInit;
/**地址栏参数 */
@@ -133,16 +141,21 @@ function beforeRequest(options: OptionsType): OptionsType | Promise<any> {
Reflect.set(options.headers, 'Accept-Language', `${language};q=0.9`);
// 是否需要设置 token
const token = getToken();
if (options.whithToken && token) {
Reflect.set(options.headers, TOKEN_KEY, TOKEN_KEY_PREFIX + token);
const accessToken = getAccessToken();
if (options.whithToken && accessToken) {
Reflect.set(
options.headers,
TOKEN_KEY,
TOKEN_KEY_PREFIX + ' ' + accessToken
);
}
// 是否需要防止数据重复提交
if (
options.repeatSubmit &&
options.dataType === 'json' &&
['post', 'put'].includes(options.method)
!(options.data instanceof FormData) &&
['POST', 'PUT'].includes(options.method)
) {
const requestObj: RepeatSubmitType = {
url: options.url,
@@ -212,13 +225,31 @@ function beforeRequest(options: OptionsType): OptionsType | Promise<any> {
return options;
}
/**请求后的拦截 */
function interceptorResponse(res: ResultType): ResultType | Promise<any> {
/**响应前的拦截 */
async function beforeResponse(
options: OptionsType,
res: ResultType
): Promise<any> {
// 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();
}
@@ -271,43 +302,45 @@ function interceptorResponse(res: ResultType): ResultType | Promise<any> {
* @returns 返回 Promise<ResultType>
*/
export async function request(options: OptionsType): Promise<ResultType> {
options = Object.assign({}, FATCH_OPTIONS, options);
let timeoutId: any = 0;
let reqOptions = Object.assign({}, FATCH_OPTIONS, options);
// 请求超时控制请求终止
if (!options.signal) {
let timeoutId: any = null;
if (!reqOptions.signal) {
const controller = new AbortController();
const { signal } = controller;
options.signal = signal;
reqOptions.signal = controller.signal;
timeoutId = setTimeout(() => {
controller.abort(); // 终止请求
}, options.timeout);
}, reqOptions.timeout);
}
// 检查请求拦截
const beforeReq = beforeRequest(options);
const beforeReq = beforeRequest(reqOptions);
if (beforeReq instanceof Promise) {
return await beforeReq;
}
options = beforeReq;
reqOptions = beforeReq;
// 判断用户传递的URL是否http或/开头
if (!options.url.startsWith('http')) {
const uri = options.url.startsWith('/') ? options.url : `/${options.url}`;
options.url = options.baseUrl + uri;
if (!reqOptions.url.startsWith('http')) {
const uri = reqOptions.url.startsWith('/')
? reqOptions.url
: `/${reqOptions.url}`;
reqOptions.url = reqOptions.baseUrl + uri;
}
try {
const res = await fetch(options.url, options);
const res = await fetch(reqOptions.url, reqOptions);
// console.log('请求结果:', res);
// 状态码拦截处理
const reqNot = stateCode(res);
if (reqNot != false) {
return reqNot;
if (res.status === 500) {
return {
code: RESULT_CODE_EXCEPTION,
msg: RESULT_MSG_SERVER_ERROR[language],
};
}
// 根据响应数据类型返回
switch (options.responseType) {
switch (reqOptions.responseType) {
case 'text': // 文本数据
const str = await res.text();
return {
@@ -317,11 +350,7 @@ export async function request(options: OptionsType): Promise<ResultType> {
case 'json': // json格式数据
const result = await res.json();
// 请求后的拦截
const beforeRes = interceptorResponse(result);
if (beforeRes instanceof Promise) {
return await beforeRes;
}
return result;
return await beforeResponse(options, result);
case 'blob': // 二进制数据则直接返回
case 'arrayBuffer':
const contentType = res.headers.get('content-type') || '';
@@ -330,7 +359,7 @@ export async function request(options: OptionsType): Promise<ResultType> {
return result as ResultType;
}
const data =
options.responseType === 'blob'
reqOptions.responseType === 'blob'
? await res.blob()
: await res.arrayBuffer();
return {
@@ -356,41 +385,7 @@ export async function request(options: OptionsType): Promise<ResultType> {
}
throw error;
} 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 { getToken } from './auth-token';
import { getAccessToken } from './auth-token';
import { localGet } from '@/utils/cache-local-utils';
import { CACHE_LOCAL_I18N } from '@/constants/cache-keys-constants';
import { TOKEN_RESPONSE_FIELD } from '@/constants/token-constants';
@@ -92,7 +92,7 @@ export class WS {
// 地址栏参数
let params = Object.assign({}, options.params, {
// 设置 token
[TOKEN_RESPONSE_FIELD]: getToken(),
[TOKEN_RESPONSE_FIELD]: getAccessToken(),
// 多语言
['language']: localGet(CACHE_LOCAL_I18N) || 'en_US',
});
@@ -119,17 +119,15 @@ export class WS {
};
// 用于指定当从服务器接受到信息时的回调函数。
ws.onmessage = ev => {
if (ev.type !== 'message') return;
// 解析文本消息
if (ev.type === 'message') {
const data = ev.data;
try {
const jsonData = JSON.parse(data);
if (typeof options.onmessage === 'function') {
options.onmessage(jsonData);
}
} catch (error) {
console.error('websocket message formatting error', error);
try {
const jsonData = JSON.parse(ev.data);
if (typeof options.onmessage === 'function') {
options.onmessage(jsonData);
}
} catch (error) {
console.error('websocket message formatting error', error);
}
};
// 用于指定连接关闭后的回调函数。
@@ -221,7 +219,7 @@ export class WS {
this.heartInterval = window.setInterval(() => {
this.send({
requestId: `${Date.now()}`,
type: 'ping',
type: 'PING',
});
}, heartTimer);
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -20,8 +20,10 @@ export const YYYYMMDDHHMMSS = 'YYYYMMDDHHmmss';
/**年-月-日 时:分:秒 列如2022-12-30 01:01:59 */
export const YYYY_MM_DD_HH_MM_SS = 'YYYY-MM-DD HH:mm:ss';
/**特殊 列如2025-04-28 08:00:46 GMT+08:00 */
export const YYYY_MM_DD_HH_MM_SSZ = 'YYYY-MM-DD HH:mm:ss [GMT]Z';
/**国际时间 列如2022-12-30T01:01:59+08:00 */
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 formatStr 时间格式 默认YYYY-MM-DD HH:mm:ssZZ
* @param formatStr 时间格式 默认 RFC3339
* @returns 时间格式字符串
*/
export function parseDateToStr(
date: string | number | Date,
formatStr: string = YYYY_MM_DD_HH_MM_SSZ
formatStr: string = RFC3339
): string {
return dayjs(date).format(formatStr);
}

View File

@@ -101,7 +101,14 @@ export function parseObjLineToHump(obj: any): any {
* @param decimalPlaces 保留小数位,默认2位
* @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'];
let i = 0;
while (bytes >= 1024 && i < units.length - 1) {

View File

@@ -27,14 +27,15 @@ export const regExpUserName = /^[A-Za-z0-9]{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,}$/;
// /^(?![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 = /^.{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位
* /^[\w\u4e00-\u9fa5-]{2,}$/
*/
export const regExpNick = /^.{2,}$/; // /^[\w\u4e00-\u9fa5-]{2,}$/;
export const regExpNick = /^.{2,}$/;
/**
* 是否为http(s)://开头

View File

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

View File

@@ -23,7 +23,9 @@ export const upfFlowData = ref<FDType>({
/**UPF-流量数据 数据解析 */
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);
upfFlowData.value.lineYUp.push(upN3[0]);
const downN6 = parseSizeFromKbs(+data['UPF.06'], 5);

View File

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

View File

@@ -14,6 +14,7 @@ import {
showPass,
getPass,
exportAll,
exportAlarm,
} from '@/api/faultManage/actAlarm';
import useI18n from '@/hooks/useI18n';
import useNeInfoStore from '@/store/modules/neinfo';
@@ -730,6 +731,38 @@ function fnModalCancel() {
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初始页数 */
function fnGetList(pageNum?: number) {
if (tableState.loading) return;
@@ -934,6 +967,11 @@ onMounted(() => {
{{ t('views.faultManage.activeAlarm.syncMyself') }}
</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
type="primary"
danger
@@ -946,19 +984,9 @@ onMounted(() => {
{{ t('views.faultManage.activeAlarm.clear') }}
</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
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 type="dashed" @click.prevent="fnExportList()">
<template #icon><ExportOutlined /></template>
{{ t('common.export') }}
</a-button>
</a-space>
</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 { ColumnsType } from 'ant-design-vue/es/table';
import {
listAct,
updateConfirm,
cancelConfirm,
exportAll,
} from '@/api/faultManage/historyAlarm';
import { listAct, exportAlarm } from '@/api/faultManage/actAlarm';
import useI18n from '@/hooks/useI18n';
import useDictStore from '@/store/modules/dict';
import useNeInfoStore from '@/store/modules/neinfo';
@@ -59,9 +59,6 @@ let rangePickerPresets = ref([
},
]);
/**表格字段列排序 */
let tableColumnsDnd = ref<ColumnsType>([]);
/**查询参数 */
let queryParams = reactive({
alarmStatus: 0,
@@ -144,7 +141,7 @@ let tableState: TabeStateType = reactive({
});
/**表格字段列 */
let tableColumns: ColumnsType = [
let tableColumns = ref<ColumnsType>([
{
title: t('views.faultManage.activeAlarm.alarmType'),
dataIndex: 'alarmType',
@@ -241,7 +238,10 @@ let tableColumns: ColumnsType = [
fixed: 'right',
width: 100,
},
];
]);
/**表格字段列排序 */
let tableColumnsDnd = ref<ColumnsType>([]);
/**表格分页器参数 */
let tablePagination = reactive({
@@ -466,7 +466,7 @@ function mapKeysWithReduce(data: any[], titleMapping: Record<string, string>) {
function fnExportAll() {
Modal.confirm({
title: 'Tip',
content: t('views.faultManage.historyAlarm.exportSure'),
content: t('views.faultManage.activeAlarm.exportSure'),
onOk() {
const key = 'exportAlarmHis';
message.loading({ content: t('common.loading'), key });
@@ -543,6 +543,38 @@ function fnModalCancel() {
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初始页数 */
function fnGetList(pageNum?: number) {
if (tableState.loading) return;
@@ -712,11 +744,17 @@ onMounted(() => {
<a-card :bordered="false" :body-style="{ padding: '0px' }">
<!-- 插槽-卡片左侧侧 -->
<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
type="primary"
@click.prevent="fnCancelConfirm()"
:disabled="state.selectedRowKeys.length <= 0"
v-if="false"
>
<template #icon>
<CloseOutlined />
@@ -727,6 +765,7 @@ onMounted(() => {
type="primary"
@click.prevent="fnExportAll()"
:disabled="tableState.data.length <= 0"
v-if="false"
>
<template #icon> <export-outlined /> </template>
{{ t('views.faultManage.activeAlarm.exportAll') }}
@@ -796,28 +835,28 @@ onMounted(() => {
:scroll="{ x: tableColumns.length * 150, y: 'calc(100vh - 480px)' }"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'origSeverity'">
<template v-if="column?.key === 'origSeverity'">
<DictTag
:options="dict.activeAlarmSeverity"
:value="record.origSeverity"
/>
</template>
<template v-if="column.key === 'alarmType'">
<template v-if="column?.key === 'alarmType'">
<DictTag
:options="dict.activeAlarmType"
:value="record.alarmType"
/>
</template>
<template v-if="column.key === 'clearType'">
<template v-if="column?.key === 'clearType'">
<DictTag
:options="dict.activeClearType"
:value="record.clearType"
/>
</template>
<template v-if="column.key === 'ackState'">
<template v-if="column?.key === 'ackState'">
<DictTag :options="dict.activeAckState" :value="record.ackState" />
</template>
<template v-if="column.key === 'id'">
<template v-if="column?.key === 'id'">
<a-space :size="8" align="center">
<a-tooltip>
<template #title>{{ t('common.viewText') }}</template>

View File

@@ -1,38 +1,57 @@
<script setup lang="ts">
import { reactive, ref, onMounted, toRaw } from 'vue';
import { reactive, ref, toRaw } from 'vue';
import { PageContainer } from 'antdv-pro-layout';
import { ProModal } from 'antdv-pro-modal';
import { SizeType } from 'ant-design-vue/es/config-provider';
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 {
getBakFile,
getBakFileList,
downFile,
delFile,
updateFTPInfo,
getFTPInfo,
putFTPInfo,
} from '@/api/logManage/exportFile';
import { RESULT_CODE_SUCCESS } from '@/constants/result-constants';
import useI18n from '@/hooks/useI18n';
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();
/**网元参数 */
let logSelect = ref<string[]>([]);
/**文件列表 */
let fileList = ref<any>([]);
/**文件来源 */
let sourceState = reactive({
/**文件列表 */
list: [
{
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({
/**读取路径 */
path: '',
/**表名 */
tableName: '',
/**当前页数 */
pageNum: 1,
/**每页条数 */
@@ -80,6 +99,9 @@ let tableColumns: ColumnsType = [
title: t('views.logManage.neFile.size'),
dataIndex: 'size',
align: 'left',
customRender(opt) {
return parseSizeFromFile(opt.value);
},
width: 100,
},
{
@@ -88,9 +110,9 @@ let tableColumns: ColumnsType = [
align: 'left',
customRender(opt) {
if (!opt.value) return '';
return parseDateToStr(opt.value * 1000);
return parseDateToStr(opt.value);
},
width: 150,
width: 200,
},
{
title: t('views.logManage.neFile.fileName'),
@@ -135,10 +157,6 @@ let tablePagination = reactive({
/**下载触发等待 */
let downLoading = ref<boolean>(false);
/**删除触发等待 */
let delLoading = ref<boolean>(false);
/**信息文件下载 */
function fnDownloadFile(row: Record<string, any>) {
if (downLoading.value) return;
@@ -150,10 +168,7 @@ function fnDownloadFile(row: Record<string, any>) {
onOk() {
downLoading.value = true;
const hide = message.loading(t('common.loading'), 0);
downFile({
path: queryParams.path,
fileName: row.fileName,
})
getFile(queryParams.path, row.fileName)
.then(res => {
if (res.code === RESULT_CODE_SUCCESS) {
message.success({
@@ -178,6 +193,9 @@ function fnDownloadFile(row: Record<string, any>) {
});
}
/**删除触发等待 */
let delLoading = ref<boolean>(false);
/**信息文件删除 */
function fnRecordDelete(row: Record<string, any>) {
if (delLoading.value) return;
Modal.confirm({
@@ -186,30 +204,27 @@ function fnRecordDelete(row: Record<string, any>) {
fileName: row.fileName,
}),
onOk() {
const key = 'delFile';
delLoading.value = true;
message.loading({ content: t('common.loading'), key });
delFile({
fileName: row.fileName,
path: queryParams.path,
})
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('views.system.user.delSuss'),
key,
content: t('common.msgSuccess', {
msg: t('common.deleteText'),
}),
duration: 2,
});
fnGetList();
} else {
message.error({
content: `${res.msg}`,
key: key,
content: t('views.logManage.exportFile.deleteTipErr'),
duration: 2,
});
}
})
.finally(() => {
hide();
delLoading.value = false;
});
},
@@ -217,17 +232,18 @@ function fnRecordDelete(row: Record<string, any>) {
}
/**网元类型选择对应修改 */
function fnNeChange(keys: any, opt: any) {
queryParams.tableName = keys;
queryParams.path = opt.path;
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.tableName === '') {
if (queryParams.path === '') {
message.warning({
content: t('views.logManage.exportFile.selectTip'),
content: t('views.logManage.exportFile.fileSourcePlease'),
duration: 2,
});
return;
@@ -237,7 +253,7 @@ function fnGetList(pageNum?: number) {
if (pageNum) {
queryParams.pageNum = pageNum;
}
getBakFileList(toRaw(queryParams)).then(res => {
listFile(toRaw(queryParams)).then(res => {
if (res.code === RESULT_CODE_SUCCESS) {
const { total, rows } = res.data;
tablePagination.total = total;
@@ -259,147 +275,34 @@ function fnGetList(pageNum?: number) {
});
}
onMounted(() => {
getBakFile().then(res => {
if (res.code === RESULT_CODE_SUCCESS) {
res.data.forEach((item: any) => {
fileList.value.push({
value: item.tableName,
label: item.tableDisplay,
path: item.filePath,
});
});
}
});
// .finally(() => {
// fnGetList();
// });
});
/**打开FTP配置窗口 */
const openFTPModal = ref<boolean>(false);
function fnFTPModalOpen() {
openFTPModal.value = !openFTPModal.value;
}
/**对象信息状态类型 */
type ModalStateType = {
/**新增框或修改框是否显示 */
openByEdit: boolean;
/**标题 */
title: string;
/**表单数据 */
from: Record<string, any>;
/**确定按钮 loading */
confirmLoading: boolean;
type FTPInfoType = {
path: string;
tag: string;
fileName: string;
};
/**FTP日志对象信息状态 */
let modalState: ModalStateType = reactive({
openByEdit: false,
title: '设置远程备份配置',
from: {
username: '',
password: '',
toIp: '',
toPort: 22,
enable: false,
dir: '',
},
confirmLoading: false,
const ftpInfo = reactive<FTPInfoType>({
path: '',
tag: '',
fileName: '',
});
/**FTP日志对象信息内表单属性和校验规则 */
const modalStateFrom = Form.useForm(
modalState.from,
reactive({
toIp: [
{
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;
/**同步文件到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.error(res.msg, 3);
modalState.title = 'Setting Remote Backup';
modalState.openByEdit = false;
message.warning(res.msg, 3);
}
});
}
/**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>
<template>
@@ -410,12 +313,14 @@ function fnSyncFileToFTP(row: Record<string, any>) {
<a-form :model="queryParams" name="queryParams" layout="horizontal">
<a-row :gutter="16" align="middle">
<a-col>
<span>{{ t('views.logManage.exportFile.fileName') }}:</span>&nbsp;
<span>{{ t('views.logManage.exportFile.fileSource') }}:</span
>&nbsp;
<a-select
v-model:value="logSelect"
:options="fileList"
v-model:value="sourceState.value"
:options="sourceState.list"
@change="fnNeChange"
:allow-clear="false"
:placeholder="t('common.selectPlease')"
style="width: 200px"
/>
</a-col>
@@ -436,9 +341,11 @@ function fnSyncFileToFTP(row: Record<string, any>) {
<!-- 插槽-卡片右侧 -->
<template #extra>
<a-space :size="8" align="center">
<a-tooltip>
<template #title>Setting Remote Backup</template>
<a-button type="text" @click.prevent="fnModalVisibleByEdit()">
<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>
@@ -465,135 +372,45 @@ function fnSyncFileToFTP(row: Record<string, any>) {
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'fileName'">
<a-space :size="8" align="center">
<a-button
type="link"
:loading="downLoading"
@click.prevent="fnSyncFileToFTP(record)"
v-if="record.fileType === 'file'"
>
<template #icon><CloudServerOutlined /></template>
</a-button>
<a-button
type="link"
:loading="downLoading"
@click.prevent="fnDownloadFile(record)"
v-if="record.fileType === 'file'"
>
<template #icon><DownloadOutlined /></template>
</a-button>
<a-button
type="link"
:loading="delLoading"
@click.prevent="fnRecordDelete(record)"
v-if="record.fileType === 'file'"
>
<template #icon><DeleteOutlined /></template>
</a-button>
<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>
<!-- 新增框或修改框 -->
<ProModal
: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>
<!-- FTP配置窗口 -->
<BackupModal v-model:open="openFTPModal"></BackupModal>
</PageContainer>
</template>

View File

@@ -97,7 +97,7 @@ let tableColumns: ColumnsType = reactive([
title: t('views.logManage.mml.logTime'),
dataIndex: 'logTime',
align: 'left',
width: 150,
width: 200,
customRender(opt) {
if (!opt.value) return '';
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 useUserStore from '@/store/modules/user';
import useAppStore from '@/store/modules/app';
import { getCaptchaImage } from '@/api/login';
import { getCaptchaImage } from '@/api/auth';
import { useRouter, useRoute } from 'vue-router';
import useI18n from '@/hooks/useI18n';
import useLayoutStore from '@/store/modules/layout';
@@ -75,18 +75,28 @@ function fnFinish() {
function fnGetCaptcha() {
if (state.captchaClick) return;
state.captchaClick = true;
getCaptchaImage().then(res => {
state.captchaClick = false;
if (res.code != 1) {
message.warning(`${res.msg}`, 3);
return;
}
state.captcha.enabled = Boolean(res.captchaEnabled);
if (state.captcha.enabled) {
state.captcha.codeImg = res.img;
state.from.uuid = res.uuid;
}
});
getCaptchaImage()
.then(res => {
if (res.code !== RESULT_CODE_SUCCESS) {
message.warning({
content: `${res.msg}`,
duration: 3,
});
return;
}
const { enabled, img, uuid } = res.data;
state.captcha.enabled = Boolean(enabled);
if (state.captcha.enabled) {
state.captcha.codeImg = img;
state.from.uuid = uuid;
}
if (res.data?.text) {
state.from.code = res.data.text;
}
})
.finally(() => {
state.captchaClick = false;
});
}
// 判断是否有背景地址

View File

@@ -181,9 +181,9 @@ function fnSendMML() {
if (res.code === RESULT_CODE_SUCCESS) {
let resultArr = res.data;
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 cmdStr = cmdArr[i];
const cmdStr = cmdArr[i] || '';
state.mmlCmdLog += `${cmdStr}\n${logStr}\n`;
}
} else {

View File

@@ -183,9 +183,9 @@ function fnSendMML() {
if (res.code === RESULT_CODE_SUCCESS) {
let resultArr = res.data;
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 cmdStr = cmdArr[i];
const cmdStr = cmdArr[i] || '';
state.mmlCmdLog += `${cmdStr}\n${logStr}\n`;
}
} else {

View File

@@ -183,9 +183,9 @@ function fnSendMML() {
if (res.code === RESULT_CODE_SUCCESS) {
let resultArr = res.data;
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 cmdStr = cmdArr[i] || "";
const cmdStr = cmdArr[i] || '';
state.mmlCmdLog += `${cmdStr}\n${logStr}\n`;
}
} else {

View File

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

View File

@@ -118,8 +118,8 @@ let tableState: TabeStateType = reactive({
let tableColumns: ColumnsType = [
{
title: t('common.rowId'),
dataIndex: 'jobLogId',
align: 'center',
dataIndex: 'logId',
align: 'left',
width: 100,
},
{
@@ -132,7 +132,7 @@ let tableColumns: ColumnsType = [
title: t('views.monitor.jobLog.jobGroup'),
dataIndex: 'jobGroup',
key: 'jobGroup',
align: 'center',
align: 'left',
width: 100,
},
{
@@ -142,17 +142,17 @@ let tableColumns: ColumnsType = [
width: 100,
},
{
title: t('views.monitor.jobLog.statusFlag'),
title: t('views.monitor.jobLog.status'),
dataIndex: 'statusFlag',
key: 'statusFlag',
align: 'center',
align: 'left',
width: 100,
},
{
title: t('views.monitor.jobLog.createTime'),
dataIndex: 'createTime',
align: 'center',
width: 150,
align: 'left',
width: 200,
customRender(opt) {
if (+opt.value <= 0) return '';
return parseDateToStr(+opt.value);
@@ -170,7 +170,7 @@ let tableColumns: ColumnsType = [
},
{
title: t('common.operate'),
key: 'jobLogId',
key: 'logId',
align: 'left',
},
];
@@ -229,7 +229,7 @@ let modalState: ModalStateType = reactive({
open: false,
title: '任务日志',
from: {
jobLogId: undefined,
logId: undefined,
jobName: '',
jobGroup: 'DEFAULT',
invokeTarget: '',
@@ -588,7 +588,7 @@ onMounted(() => {
<!-- 表格列表 -->
<a-table
class="table"
row-key="jobLogId"
row-key="logId"
:columns="tableColumns"
:loading="tableState.loading"
:data-source="tableState.data"
@@ -615,7 +615,7 @@ onMounted(() => {
}}
</a-tag>
</template>
<template v-if="column.key === 'jobLogId'">
<template v-if="column.key === 'logId'">
<a-space :size="8" align="center">
<a-tooltip>
<template #title>{{ t('common.viewText') }}</template>
@@ -644,8 +644,8 @@ onMounted(() => {
<a-form layout="horizontal" :label-col="{ span: 6 }" :label-wrap="true">
<a-row>
<a-col :lg="12" :md="12" :xs="24">
<a-form-item :label="t('common.rowId')" name="jobLogId">
{{ modalState.from.jobLogId }}
<a-form-item :label="t('common.rowId')" name="logId">
{{ modalState.from.logId }}
</a-form-item>
</a-col>
<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 { SizeType } from 'ant-design-vue/es/config-provider';
import { MenuInfo } from 'ant-design-vue/es/menu/src/interface';
import BackupModal from './components/BackupModal.vue';
import useNeInfoStore from '@/store/modules/neinfo';
import useI18n from '@/hooks/useI18n';
import useDictStore from '@/store/modules/dict';
import { NE_TYPE_LIST } from '@/constants/ne-constants';
import { RESULT_CODE_SUCCESS } from '@/constants/result-constants';
import { parseDateToStr } from '@/utils/date-utils';
import { regExpIPv4 } from '@/utils/regular-utils';
import {
delNeConfigBackup,
downNeConfigBackup,
listNeConfigBackup,
updateNeConfigBackup,
getFTPInfo,
putFTPInfo,
updateFTPInfo,
} from '@/api/ne/neConfigBackup';
import { pushBackupFTP } from '@/api/neData/backup';
import saveAs from 'file-saver';
const { t } = useI18n();
const { getDict } = useDictStore();
@@ -389,119 +387,26 @@ onMounted(() => {
});
});
/**FTP日志对象信息状态 */
let modalStateFTP: ModalStateType = reactive({
openByEdit: false,
title: '设置远程备份配置',
from: {
username: '',
password: '',
toIp: '',
toPort: 22,
enable: false,
dir: '',
},
confirmLoading: false,
});
/**打开FTP配置窗口 */
const openFTPModal = ref<boolean>(false);
function fnFTPModalOpen() {
openFTPModal.value = !openFTPModal.value;
}
/**FTP日志对象信息内表单属性和校验规则 */
const modalStateFTPFrom = Form.useForm(
modalStateFTP.from,
reactive({
toIp: [
{
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 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;
/**同步文件到FTP */
function fnSyncFileToFTP(row: Record<string, any>) {
pushBackupFTP({
path: row.path.substring(0, row.path.lastIndexOf('/')),
fileName: row.name,
tag: 'ne_config',
}).then(res => {
if (res.code === RESULT_CODE_SUCCESS) {
message.success(t('common.operateOk'), 3);
} else {
message.error(res.msg, 3);
modalStateFTP.title = 'Setting Remote Backup';
modalStateFTP.openByEdit = false;
message.warning(res.msg, 3);
}
});
}
/**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>
<template>
@@ -572,8 +477,10 @@ function fnSyncFileToFTP(row: Record<string, any>) {
<template #extra>
<a-space :size="8" align="center">
<a-tooltip placement="topRight">
<template #title>Setting Remote Backup</template>
<a-button type="text" @click.prevent="fnModalFTPVisibleByEdit()">
<template #title>
{{ t('views.ne.neConfigBackup.backupModal.title') }}
</template>
<a-button type="text" @click.prevent="fnFTPModalOpen()">
<template #icon><DeliveredProcedureOutlined /></template>
</a-button>
</a-tooltip>
@@ -632,12 +539,10 @@ function fnSyncFileToFTP(row: Record<string, any>) {
<template v-if="column.key === 'id'">
<a-space :size="8" align="center">
<a-tooltip placement="topRight">
<template #title>Send Current File To Remote Backup</template>
<a-button
type="link"
:loading="modalStateFTP.confirmLoading"
@click.prevent="fnSyncFileToFTP(record)"
>
<template #title>
{{ t('views.ne.neConfigBackup.backupModal.pushFileOper') }}
</template>
<a-button type="link" @click.prevent="fnSyncFileToFTP(record)">
<template #icon><CloudServerOutlined /></template>
</a-button>
</a-tooltip>
@@ -676,7 +581,7 @@ function fnSyncFileToFTP(row: Record<string, any>) {
<!-- 新增框或修改框 -->
<ProModal
:drag="true"
:width="512"
:width="520"
:destroyOnClose="true"
:keyboard="false"
:mask-closable="false"
@@ -719,105 +624,8 @@ function fnSyncFileToFTP(row: Record<string, any>) {
</a-form>
</ProModal>
<!-- FTP -->
<ProModal
: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>
<!-- FTP配置窗口 -->
<BackupModal v-model:open="openFTPModal"></BackupModal>
</PageContainer>
</template>

View File

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

View File

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

View File

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

View File

@@ -780,11 +780,12 @@ onMounted(() => {
<a-form :model="queryParams" name="queryParams" layout="horizontal">
<a-row :gutter="16">
<a-col :lg="6" :md="12" :xs="24">
<a-form-item :label="t('views.neUser.auth.neType')" name="neId ">
<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>
@@ -1268,7 +1269,6 @@ onMounted(() => {
</a-button>
</a-col>
</a-row>
<a-input-password
v-if="uploadImportState.from.typeVal === 'k4'"
v-model:value="uploadImportState.from.typeData"

View File

@@ -1282,11 +1282,12 @@ onMounted(() => {
<a-form :model="queryParams" name="queryParams" layout="horizontal">
<a-row :gutter="16">
<a-col :lg="6" :md="12" :xs="24">
<a-form-item :label="t('views.neUser.sub.neType')" name="neId ">
<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>
@@ -2284,7 +2285,6 @@ onMounted(() => {
</a-button>
</a-col>
</a-row>
<a-alert
:message="uploadImportState.msg"
: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 { Modal, message } from 'ant-design-vue/es';
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 { useRouter, useRoute } from 'vue-router';
import useAppStore from '@/store/modules/app';

View File

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

View File

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

View File

@@ -2,8 +2,7 @@
import { onMounted } from 'vue';
import { useRouter } from 'vue-router';
import { RESULT_CODE_SUCCESS } from '@/constants/result-constants';
import { TOKEN_RESPONSE_FIELD } from '@/constants/token-constants';
import { getToken, setToken } from '@/plugins/auth-token';
import { getAccessToken, setAccessToken } from '@/plugins/auth-token';
import { bootloaderStart } from '@/api/system/quick-start/bootloader';
import { fnToStepName } from '../hooks/useStep';
import useI18n from '@/hooks/useI18n';
@@ -19,11 +18,10 @@ function fnChangeLocale(e: any) {
/**引导开始 */
function fnGuideStart() {
if (getToken()) return;
if (getAccessToken()) return;
bootloaderStart().then(res => {
if (res.code === RESULT_CODE_SUCCESS && res.data) {
const token = res.data[TOKEN_RESPONSE_FIELD];
setToken(token);
setAccessToken(res.data.accessToken, res.data.expiresIn);
} else {
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 { RESULT_CODE_SUCCESS } from '@/constants/result-constants';
import useI18n from '@/hooks/useI18n';
import useUserStore from '@/store/modules/user';
const { t } = useI18n();
const { getDict } = useDictStore();
const userStore = useUserStore();
const router = useRouter();
const route = useRoute();
const routePath = route.path;
@@ -732,7 +734,7 @@ function fnGetList(pageNum?: number) {
queryParams.beginTime = queryRangePicker.value[0];
queryParams.endTime = queryRangePicker.value[1];
listRole(toRaw(queryParams)).then(res => {
if (res.code === RESULT_CODE_SUCCESS) {
if (res.code === RESULT_CODE_SUCCESS) {
// 取消勾选
if (tableState.selectedRowKeys.length > 0) {
tableState.selectedRowKeys = [];
@@ -941,6 +943,7 @@ onMounted(() => {
v-if="
dict.sysNormalDisable.length > 0 &&
record.roleId !== 1 &&
!userStore.roles?.includes(record.roleKey) &&
hasPermissions(['system:role:edit'])
"
v-model:checked="record.statusFlag"
@@ -969,7 +972,12 @@ onMounted(() => {
<template #icon><ProfileOutlined /></template>
</a-button>
</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>
<a-button
type="link"
@@ -979,7 +987,12 @@ onMounted(() => {
<template #icon><FormOutlined /></template>
</a-button>
</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>
<a-button
type="link"
@@ -1001,7 +1014,7 @@ onMounted(() => {
<template #icon><SecurityScanOutlined /></template>
</a-button>
</a-tooltip>
<a-tooltip placement="topRight" v-if="record.roleId !== 1">
<a-tooltip placement="topRight" v-if="false">
<template #title>{{
t('views.system.role.distributeUser')
}}</template>

View File

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

View File

@@ -7,7 +7,7 @@ import { Modal, message } from 'ant-design-vue/es';
import { parseDateToStr } from '@/utils/date-utils';
import { getNeDirZip, getNeFile, listNeFiles } from '@/api/tool/neFile';
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 useTabsStore from '@/store/modules/tabs';
import useI18n from '@/hooks/useI18n';

View File

@@ -1,35 +1,32 @@
<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 { 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 DissectionDump from '../tshark/components/DissectionDump.vue';
import PacketTable from '../tshark/components/PacketTable.vue';
import { usePCAP, NO_SELECTION } from '../tshark/hooks/usePCAP';
import {
RESULT_CODE_ERROR,
RESULT_CODE_SUCCESS,
} from '@/constants/result-constants';
import { filePullTask } from '@/api/trace/task';
import { OptionsType, WS } from '@/plugins/ws-websocket';
import { filePullTask, getTraceData, listTraceData } from '@/api/trace/task';
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 useDictStore from '@/store/modules/dict';
import useTabsStore from '@/store/modules/tabs';
import saveAs from 'file-saver';
import { parseDateToStr } from '@/utils/date-utils';
const { getDict } = useDictStore();
const { t } = useI18n();
const route = useRoute();
const router = useRouter();
const tabsStore = useTabsStore();
const ws = new WS();
const { t } = useI18n();
const {
state,
handleSelectedTreeEntry,
handleSelectedFindSelection,
handleSelectedFrame,
handleScrollBottom,
handleFilterFrames,
handleLoadFile,
} = usePCAP();
const ws = new wsUtil.WS();
const wk = new wkUtil.WK();
/**跟踪编号 */
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);
@@ -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) {
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>) {
const { code, requestId, data } = res;
@@ -101,7 +360,6 @@ function wsMessage(res: Record<string, any>) {
// 建联时发送请求
if (!requestId && data.clientId) {
fnFilePCAP();
return;
}
@@ -110,13 +368,16 @@ function wsMessage(res: Record<string, any>) {
return;
}
if (data.groupId === `2_${traceId.value}`) {
fnFilePCAP();
// 第一页降序时实时添加记录
if (queryParams.pageNum === 1 && queryParams.sortOrder === 'desc') {
tableState.data.unshift(data);
}
tablePagination.total += 1;
}
}
/**建立WS连接 */
function fnWS() {
const options: OptionsType = {
const options: wsUtil.OptionsType = {
url: '/ws',
params: {
/**订阅通道组
@@ -135,15 +396,167 @@ function fnWS() {
ws.connect(options);
}
watch(
() => state.initialized,
v => {
v && fnWS();
// =========== WK ==============
const NO_SELECTION = { id: '', idx: 0, start: 0, length: 0 };
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(() => {
ws.close();
wk.send({ type: 'close' }) && wk.close();
if (ws.state() <= WebSocket.OPEN) ws.close();
});
</script>
@@ -154,8 +567,9 @@ onBeforeUnmount(() => {
:loading="!state.initialized"
: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()">
<template #icon><CloseOutlined /></template>
{{ t('common.close') }}
@@ -173,89 +587,65 @@ onBeforeUnmount(() => {
<strong>{{ traceId }}</strong>
</span>
</a-space>
</template>
<div class="toolbar-info">
<a-tag color="green" v-show="!!state.currentFilter">
{{ state.currentFilter }}
</a-tag>
<span> Matched Frame: {{ state.totalFrames }} </span>
</div>
<!-- 包信息 -->
<a-popover
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"
/>
<!-- 插槽-卡片右侧 -->
<template #extra>
<a-tooltip>
<template #title>{{ t('common.reloadText') }}</template>
<a-button type="text" @click.prevent="fnGetList()">
<template #icon><ReloadOutlined /></template>
</a-button>
</a-tooltip>
</template>
<!-- 包数据表 -->
<PacketTable
:columns="state.columns"
:data="state.packetFrames"
:selectedFrame="state.selectedFrame"
:onSelectedFrame="handleSelectedFrame"
:onScrollBottom="handleScrollBottom"
></PacketTable>
<a-row>
<a-table
class="table"
row-key="id"
:columns="tableColumns"
:loading="tableState.loading"
:data-source="tableState.data"
:size="tableState.size"
:pagination="tablePagination"
: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">
<!-- 帧数据 -->
<DissectionTree
id="root"
:select="handleSelectedTreeEntry"
:selected="state.selectedTreeEntry"
:tree="state.selectedPacket.tree"
:select="handleSelectedTree"
:selected="state.selectedTree"
:tree="state.packetFrame.tree"
/>
</a-col>
<a-col :lg="12" :md="12" :xs="24" class="dump">
@@ -268,15 +658,15 @@ onBeforeUnmount(() => {
<a-tab-pane
:key="idx"
:tab="v.name"
v-for="(v, idx) in state.selectedPacket.data_sources"
v-for="(v, idx) in state.packetFrame.data_sources"
style="overflow: auto"
>
<DissectionDump
:base64="v.data"
:select="(pos:number)=>handleSelectedFindSelection(idx, pos)"
:selected="
idx === state.selectedTreeEntry.idx
? state.selectedTreeEntry
idx === state.selectedTree.idx
? state.selectedTree
: NO_SELECTION
"
/>
@@ -289,24 +679,20 @@ onBeforeUnmount(() => {
</template>
<style scoped>
.toolbar {
display: flex;
align-items: center;
margin-bottom: 12px;
.table :deep(.ant-pagination) {
padding: 0 24px;
}
.toolbar-info {
flex: 1;
text-align: right;
padding-right: 8px;
.table :deep(.table-striped-select) td {
background-color: #c2c2c2;
cursor: pointer;
}
.summary {
display: flex;
flex-direction: column;
.table :deep(.table-striped-recv) td {
background-color: #a9e2ff;
cursor: pointer;
}
.summary-item > span:first-child {
font-weight: 600;
margin-right: 6px;
.table :deep(.table-striped-send) td {
background-color: #bcfbb3;
cursor: pointer;
}
.tree {

View File

@@ -1,24 +1,60 @@
<script setup lang="ts">
import { reactive, onMounted, toRaw } from 'vue';
import { reactive, onMounted, toRaw, ref } from 'vue';
import { PageContainer } from 'antdv-pro-layout';
import { ProModal } from 'antdv-pro-modal';
import { Modal } from 'ant-design-vue/es';
import { SizeType } from 'ant-design-vue/es/config-provider';
import { MenuInfo } from 'ant-design-vue/es/menu/src/interface';
import { ColumnsType } from 'ant-design-vue/es/table';
import { parseDateToStr } from '@/utils/date-utils';
import { RESULT_CODE_SUCCESS } from '@/constants/result-constants';
import { 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 { 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 { 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({
/**移动号 */
imsi: '',
/**移动号 */
msisdn: '',
traceId: traceId.value,
sortBy: 'timestamp',
sortOrder: 'asc',
/**开始时间 */
beginTime: undefined as undefined | number,
/**结束时间 */
endTime: undefined as undefined | number,
/**当前页数 */
pageNum: 1,
/**每页条数 */
@@ -28,10 +64,10 @@ let queryParams = reactive({
/**查询参数重置 */
function fnQueryReset() {
queryParams = Object.assign(queryParams, {
imsi: '',
pageNum: 1,
pageSize: 20,
});
queryRangePicker.value = undefined;
tablePagination.current = 1;
tablePagination.pageSize = 20;
fnGetList();
@@ -43,8 +79,6 @@ type TabeStateType = {
loading: boolean;
/**紧凑型 */
size: SizeType;
/**搜索栏 */
seached: boolean;
/**记录数据 */
data: object[];
};
@@ -52,66 +86,87 @@ type TabeStateType = {
/**表格状态 */
let tableState: TabeStateType = reactive({
loading: false,
size: 'middle',
seached: true,
size: 'small',
data: [],
});
/**表格字段列 */
let tableColumns: ColumnsType = [
{
title: t('views.traceManage.analysis.trackTaskId'),
dataIndex: 'taskId',
align: 'center',
title: t('common.rowId'),
dataIndex: 'id',
align: 'left',
width: 100,
},
{
title: t('views.traceManage.analysis.imsi'),
dataIndex: 'imsi',
align: 'center',
title: t('views.traceManage.task.msgNe'),
dataIndex: 'msgNe',
align: 'left',
width: 150,
},
{
title: t('views.traceManage.analysis.msisdn'),
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'),
title: t('views.traceManage.task.rowTime'),
dataIndex: 'timestamp',
align: 'center',
align: 'left',
width: 250,
customRender(opt) {
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'),
key: 'id',
align: 'center',
align: 'left',
},
];
@@ -148,6 +203,18 @@ function fnTableSize({ key }: MenuInfo) {
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初始页数 */
function fnGetList(pageNum?: number) {
if (tableState.loading) return;
@@ -155,6 +222,19 @@ function fnGetList(pageNum?: number) {
if (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 => {
if (res.code === RESULT_CODE_SUCCESS) {
const { total, rows } = res.data;
@@ -199,24 +279,17 @@ let modalState: ModalStateType = reactive({
* @param row 记录信息
*/
function fnModalVisible(row: Record<string, any>) {
//
const hexString = parseBase64Data(row.rawMsg);
const rawData = convertToReadableFormat(hexString);
modalState.from.rawData = rawData;
// RAWHTML
// getTraceRawInfo(row.id).then(res => {
// if (res.code === RESULT_CODE_SUCCESS) {
// const htmlString = rawDataHTMLScript(res.msg);
// modalState.from.rawDataHTML = htmlString;
// modalState.from.downBtn = true;
// } else {
// modalState.from.rawDataHTML = t('views.traceManage.analysis.noData');
// }
// });
modalState.title = t('views.traceManage.analysis.taskTitle', {
num: row.imsi,
getTraceData(row.id).then(res => {
if (res.code === RESULT_CODE_SUCCESS) {
Object.assign(modalState.from, res.data);
//
const hexString = parseBase64Data(res.data.rawMsg);
const rawData = convertToReadableFormat(hexString);
modalState.from.rawData = rawData;
modalState.title = t('views.traceManage.task.taskInfo');
modalState.open = true;
}
});
modalState.open = true;
}
/**
@@ -224,15 +297,13 @@ function fnModalVisible(row: Record<string, any>) {
*/
function fnModalVisibleClose() {
modalState.open = false;
modalState.from.downBtn = false;
modalState.from.rawDataHTML = '';
modalState.from.rawData = '';
}
// Base64
function parseBase64Data(hexData: string) {
function parseBase64Data(base64Data: string) {
// Base64
const byteString = atob(hexData);
const byteString = decode(base64Data);
const byteArray = new Uint8Array(byteString.length);
for (let i = 0; i < byteString.length; i++) {
byteArray[i] = byteString.charCodeAt(i);
@@ -252,7 +323,7 @@ function convertToReadableFormat(hexString: string) {
let result = '';
let asciiResult = '';
let arr = [];
let row = 100;
let row = 10000;
for (let i = 0; i < hexString.length; i += 2) {
const hexChars = hexString.substring(i, i + 2);
const decimal = parseInt(hexChars, 16);
@@ -286,100 +357,46 @@ function convertToReadableFormat(hexString: string) {
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(() => {
//
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>
<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-col :lg="8" :md="12" :xs="24">
<a-form-item
:label="t('views.traceManage.analysis.imsi')"
name="imsi"
:label="t('views.traceManage.task.rowTime')"
name="queryRangePicker"
>
<a-input
v-model:value="queryParams.imsi"
:allow-clear="true"
:placeholder="t('views.traceManage.analysis.imsiPlease')"
></a-input>
</a-form-item>
</a-col>
<a-col :lg="6" :md="12" :xs="24">
<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-range-picker
v-model:value="queryRangePicker"
:bordered="true"
:allow-clear="false"
style="width: 100%"
:show-time="{ format: 'HH:mm:ss' }"
format="YYYY-MM-DD HH:mm:ss"
></a-range-picker>
</a-form-item>
</a-col>
<a-col :lg="6" :md="12" :xs="24">
@@ -402,20 +419,22 @@ onMounted(() => {
<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>
<a-space :size="8" align="center">
<a-tooltip>
<template #title>{{ t('common.searchBarText') }}</template>
<a-switch
v-model:checked="tableState.seached"
:checked-children="t('common.switch.show')"
:un-checked-children="t('common.switch.hide')"
size="small"
/>
</a-tooltip>
<a-tooltip>
<template #title>{{ t('common.reloadText') }}</template>
<a-button type="text" @click.prevent="fnGetList()">
@@ -459,11 +478,18 @@ onMounted(() => {
:size="tableState.size"
:pagination="tablePagination"
:scroll="{ x: true }"
@change="fnTableChange"
>
<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'">
<a-space :size="8" align="center">
<a-tooltip>
<a-tooltip placement="topRight">
<template #title>{{ t('common.viewText') }}</template>
<a-button type="link" @click.prevent="fnModalVisible(record)">
<template #icon><ProfileOutlined /></template>
@@ -482,31 +508,111 @@ onMounted(() => {
:title="modalState.title"
:open="modalState.open"
@cancel="fnModalVisibleClose"
:footer="false"
>
<div class="raw-title">
{{ t('views.traceManage.analysis.signalData') }}
</div>
<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-divider />
<!-- <div class="raw-title">
{{ t('views.traceManage.analysis.signalDetail') }}
<a-button
type="dashed"
size="small"
@click.prevent="fnDownloadFile"
v-if="modalState.from.downBtn"
<a-form
name="modalStateFrom"
layout="horizontal"
:label-col="{ span: 8 }"
:label-wrap="true"
>
<a-row>
<a-col :lg="12" :md="12" :xs="24">
<a-form-item
:label="t('views.traceManage.task.msgType')"
name="msgType"
>
<DictTag
:options="dict.traceMsgType"
:value="modalState.from.msgType"
/>
</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>
<DownloadOutlined />
</template>
{{ t('views.traceManage.analysis.taskDownText') }}
</a-button>
</div> -->
<!-- <div class="raw-html" v-html="modalState.from.rawDataHTML"></div> -->
{{ modalState.from.imsi }}
</a-form-item>
<a-form-item
v-if="modalState.from.ifType"
:label="t('views.traceManage.task.protocolOrInterface')"
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>
</PageContainer>
</template>
@@ -517,24 +623,15 @@ onMounted(() => {
}
.raw {
&-title {
color: #000000d9;
font-size: 24px;
line-height: 1.8;
}
.num {
background-color: #e5e5e5;
background-color: #f0f0f0;
}
.code {
background-color: #e7e6ff;
background-color: #0078d4;
color: #fff;
}
.txt {
background-color: #ffe3e5;
}
&-html {
max-height: 300px;
overflow-y: auto;
background-color: #d9d9d9;
}
}
</style>

View File

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

View File

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

View File

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

View File

@@ -8,11 +8,12 @@ import DissectionDump from './components/DissectionDump.vue';
import PacketTable from './components/PacketTable.vue';
import { usePCAP, NO_SELECTION } from './hooks/usePCAP';
import { parseSizeFromFile } from '@/utils/parse-utils';
import { parseDateToStr } from '@/utils/date-utils';
import useI18n from '@/hooks/useI18n';
const { t } = useI18n();
const {
state,
handleSelectedTreeEntry,
handleSelectedTree,
handleSelectedFindSelection,
handleSelectedFrame,
handleScrollBottom,
@@ -49,8 +50,9 @@ function fnUpload(up: UploadRequestOption) {
:loading="!state.initialized"
:body-style="{ padding: '12px' }"
>
<div class="toolbar">
<a-space :size="8" class="toolbar-oper">
<!-- 插槽-卡片左侧侧 -->
<template #title>
<a-space :size="8" align="center">
<a-upload
name="file"
list-type="picture"
@@ -64,61 +66,94 @@ function fnUpload(up: UploadRequestOption) {
</a-upload>
<a-button @click="handleLoadExample()">Example</a-button>
</a-space>
<div class="toolbar-info">
</template>
<!-- 插槽-卡片右侧 -->
<template #extra>
<a-space :size="8" align="center">
<a-tag color="green" v-show="!!state.currentFilter">
{{ state.currentFilter }}
</a-tag>
<span> Matched Frame: {{ state.totalFrames }} </span>
</div>
<!-- 包信息 -->
<a-popover
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>
<!-- 包信息 -->
<a-popover
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>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 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>Duration:</span>
<span>{{ Math.round(state.summary.elapsed_time) }}s</span>
</div>
</div>
</template>
<InfoCircleOutlined />
</a-popover>
</div>
</template>
<InfoCircleOutlined />
</a-popover>
</a-space>
</template>
<!-- 包数据表过滤 -->
<a-input-group compact>
<a-input
<a-auto-complete
v-model:value="state.filter"
placeholder="display filter, example: tcp"
:allow-clear="true"
:options="[
{ 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)"
@pressEnter="handleFilterFrames"
:allow-clear="true"
>
<template #prefix>
<FilterOutlined />
</template>
</a-input>
<a-input
placeholder="display filter, example: tcp"
@pressEnter="handleFilterFrames"
>
<template #prefix>
<FilterOutlined />
</template>
</a-input>
</a-auto-complete>
<a-button
type="primary"
html-type="submit"
@@ -143,14 +178,18 @@ function fnUpload(up: UploadRequestOption) {
:onScrollBottom="handleScrollBottom"
></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">
<!-- 帧数据 -->
<DissectionTree
id="root"
:select="handleSelectedTreeEntry"
:selected="state.selectedTreeEntry"
:tree="state.selectedPacket.tree"
:select="handleSelectedTree"
:selected="state.selectedTree"
:tree="state.packetFrame.tree"
/>
</a-col>
<a-col :lg="12" :md="12" :xs="24" class="dump">
@@ -163,15 +202,15 @@ function fnUpload(up: UploadRequestOption) {
<a-tab-pane
:key="idx"
:tab="v.name"
v-for="(v, idx) in state.selectedPacket.data_sources"
v-for="(v, idx) in state.packetFrame.data_sources"
style="overflow: auto"
>
<DissectionDump
:base64="v.data"
:select="(pos:number)=>handleSelectedFindSelection(idx, pos)"
:selected="
idx === state.selectedTreeEntry.idx
? state.selectedTreeEntry
idx === state.selectedTree.idx
? state.selectedTree
: NO_SELECTION
"
/>
@@ -184,17 +223,6 @@ function fnUpload(up: UploadRequestOption) {
</template>
<style scoped>
.toolbar {
display: flex;
align-items: center;
margin-bottom: 12px;
}
.toolbar-info {
flex: 1;
text-align: right;
padding-right: 8px;
}
.summary {
display: flex;
flex-direction: column;

View File

@@ -1,16 +1,16 @@
<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 { message, Modal } from 'ant-design-vue/es';
import DissectionTree from '../tshark/components/DissectionTree.vue';
import DissectionDump from '../tshark/components/DissectionDump.vue';
import PacketTable from '../tshark/components/PacketTable.vue';
import {
RESULT_CODE_ERROR,
RESULT_CODE_SUCCESS,
} from '@/constants/result-constants';
import { filePullTask } from '@/api/trace/task';
import { OptionsType, WS } from '@/plugins/ws-websocket';
import { 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 saveAs from 'file-saver';
import {
@@ -19,34 +19,20 @@ import {
packetStop,
packetFilter,
packetKeep,
packetPCAPFile,
} 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();
// =========== WK ==============
const NO_SELECTION = { id: '', idx: 0, start: 0, length: 0 };
type StateType = {
/**网卡设备列表 */
devices: { id: string; label: string; children: any[] }[];
/**初始化 */
initialized: boolean;
/**保活调度器 */
keepTimer: any;
/**任务 */
task: {
taskNo: string;
device: string;
filter: string;
outputPCAP: boolean;
};
/**字段 */
columns: string[];
/**过滤条件 */
filter: string;
/**过滤条件错误信息 */
filterError: string | null;
/**当前选中的帧编号 */
selectedFrame: number;
/**当前选中的帧数据 */
@@ -57,63 +43,19 @@ type StateType = {
selectedTree: typeof NO_SELECTION;
/**选择帧的Dump数据标签 */
selectedDataSourceIndex: number;
/**包总数 */
totalPackets: number;
/**包数据 */
packetList: any[];
};
const state = reactive<StateType>({
devices: [],
initialized: false,
keepTimer: null,
task: {
taskNo: 'laYlTbq',
device: '192.168.5.58',
filter: 'tcp and (port 33030 or 8080)',
outputPCAP: false,
},
columns: [
'No.',
'Time',
'Source',
'Destination',
'Protocol',
'Length',
'Info',
],
filter: 'tcp and (port 33030 or 8080)',
filterError: null,
selectedFrame: 1,
selectedFrame: 0,
/**当前选中的帧数据 */
packetFrame: { tree: [], data_sources: [] },
packetFrameTreeMap: null, // 注意Map 需要额外处理
selectedTree: NO_SELECTION, // NO_SELECTION 需要定义
/**选择帧的Dump数据标签 */
selectedDataSourceIndex: 0,
// 包数据
totalPackets: 0,
packetList: [],
});
/**清除帧数据和报文信息状态 */
function fnReset() {
state.initialized = false;
// 选择帧的数据
state.selectedFrame = 0;
state.packetFrame = { tree: [], data_sources: [] };
state.packetFrameTreeMap = null;
state.selectedTree = NO_SELECTION;
state.selectedDataSourceIndex = 0;
// 过滤条件
state.filter = 'tcp and (port 33030 or 8080)';
state.filterError = null;
// 包数据
state.totalPackets = 0;
state.packetList = [];
}
/**解析帧数据为简单结构 */
function parseFrameTree(id: string, node: Record<string, any>) {
let map = new Map();
@@ -136,15 +78,9 @@ function parseFrameTree(id: string, node: Record<string, any>) {
return map;
}
/**帧数据点击选中 */
function handleSelectedTreeEntry(e: any) {
console.log('fnSelectedTreeEntry', e);
state.selectedTree = e;
}
/**报文数据点击选中 */
function handleSelectedFindSelection(src_idx: number, pos: number) {
console.log('fnSelectedFindSelection', pos);
// console.log('fnSelectedFindSelection', pos);
if (state.packetFrameTreeMap == null) return;
// find the smallest one
let current = null;
@@ -166,83 +102,274 @@ function handleSelectedFindSelection(src_idx: number, pos: number) {
state.selectedTree = state.packetFrameTreeMap.get(current);
}
}
/**帧数据点击选中 */
function handleSelectedTree(e: any) {
state.selectedTree = e;
}
/**包数据表点击选中 */
function handleSelectedFrame(num: number) {
console.log('fnSelectedFrame', num, state.totalPackets);
const packet = state.packetList.find((v: any) => v.number === num);
if (!packet) return;
const packetFrame = packet.frame;
state.selectedFrame = packet.number;
state.packetFrame = packetFrame;
state.packetFrameTreeMap = parseFrameTree('root', packetFrame);
state.selectedTree = NO_SELECTION;
state.selectedDataSourceIndex = 0;
/**接收数据后回调 */
function 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;
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;
}
}
/**包数据表滚动底部加载 */
function handleScrollBottom(index: any) {
console.log('handleScrollBottom', index);
/**建立WK连接 */
function fnWK() {
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() {
// state.task.taskNo = 'laYlTbq';
state.task.taskNo = Number(Date.now()).toString(16);
state.task.outputPCAP = false;
packetStart(state.task).then(res => {
if (res.code === RESULT_CODE_SUCCESS) {
fnReset();
fnWS();
} else {
message.error(t('common.operateErr'), 3);
}
});
if (taskState.keepTimer) return;
taskState.keepTimer = true;
const hide = message.loading(t('common.loading'), 0);
// taskState.task.taskNo = 'laYlTbq';
taskState.task.taskNo = Number(Date.now()).toString(16);
taskState.task.filter = taskState.filter;
packetStart(toRaw(taskState.task))
.then(res => {
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() {
packetStop(state.task.taskNo).then(res => {
if (res.code === RESULT_CODE_SUCCESS) {
if (typeof taskState.keepTimer !== 'number') return;
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();
state.initialized = false;
state.filter = '';
state.filterError = null;
} else {
message.warning(res.msg, 3);
}
});
clearInterval(taskState.keepTimer);
taskState.keepTimer = null;
taskState.filter = '';
taskState.filterError = null;
taskState.stop = true;
})
.finally(() => {
hide();
});
}
/**跟踪数据表过滤 */
function handleFilterFrames() {
packetFilter(state.task.taskNo, state.filter).then(res => {
packetFilter(taskState.task.taskNo, taskState.filter).then(res => {
if (res.code === RESULT_CODE_SUCCESS) {
state.task.filter = state.filter;
taskState.task.filter = taskState.filter;
taskState.filterError = null;
} else {
state.filterError = res.msg;
taskState.filterError = res.msg;
}
});
}
/**开始跟踪 */
/**选择网卡 */
function fnDevice(v: string) {
state.task.device = v;
taskState.task.device = v;
}
/**下载触发等待 */
let downLoading = ref<boolean>(false);
/**信息文件下载 */
function fnDownloadPCAP() {
if (downLoading.value) return;
const fileName = `trace_packet_${state.task.taskNo}.pcap`;
const fileName = `packet_${taskState.task.taskNo}.pcap`;
Modal.confirm({
title: t('common.tipTitle'),
content: t('views.logManage.neFile.downTip', { fileName }),
onOk() {
downLoading.value = true;
const hide = message.loading(t('common.loading'), 0);
filePullTask(state.task.taskNo)
packetPCAPFile(taskState.task.taskNo)
.then(res => {
if (res.code === RESULT_CODE_SUCCESS) {
message.success({
@@ -267,90 +394,203 @@ function fnDownloadPCAP() {
});
}
/**接收数据后回调 */
function wsMessage(res: Record<string, any>) {
const { code, requestId, data } = res;
if (code === RESULT_CODE_ERROR) {
console.warn(res.msg);
return;
}
// =========== 表格数据 ==============
/**表格状态类型 */
type TabeStateType = {
/**加载等待 */
loading: boolean;
/**记录数据 */
data: Record<string, any>[];
/**总记录数 */
total: number;
/**选择帧编号 */
selectedNumber: number;
};
// 建联时发送请求
if (!requestId && data.clientId) {
state.initialized = true;
state.keepTimer = setInterval(() => {
packetKeep(state.task.taskNo, 120);
}, 90 * 1000);
return;
}
/**表格状态 */
let tableState: TabeStateType = reactive({
loading: false,
data: [],
total: 0,
selectedNumber: 0,
});
// 订阅组信息
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;
}
if (data.groupId === `4_${state.task.taskNo}`) {
const packetData = data.data;
state.totalPackets = packetData.number;
state.packetList.push(packetData);
}
tableState.selectedNumber = row.number;
const blob = generatePCAP(row.time / 1e3, row.data);
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);
}
/**建立WS连接 */
function fnWS() {
const options: OptionsType = {
url: '/ws',
params: {
/**订阅通道组
*
* 信令跟踪Packet (GroupID:4_taskNo)
*/
subGroupID: `4_${state.task.taskNo}`,
},
onmessage: wsMessage,
onerror: (ev: any) => {
// 接收数据后回调
console.error(ev);
},
};
//建立连接
ws.connect(options);
// 2. 创建PCAP文件头
const fileHeader = new Uint8Array([
0xd4,
0xc3,
0xb2,
0xa1, // magic_number (微秒级)
0x02,
0x00, // version_major (2)
0x04,
0x00, // version_minor (4)
0x00,
0x00,
0x00,
0x00, // thiszone (UTC)
0x00,
0x00,
0x00,
0x00, // sigfigs (固定0)
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(() => {
packetDevices().then(res => {
if (res.code === RESULT_CODE_SUCCESS) {
state.devices = res.data;
taskState.devices = res.data;
if (res.data.length === 0) return;
state.task.device = res.data[0].id;
taskState.task.device = res.data[0].id;
}
});
fnWK();
});
onBeforeUnmount(() => {
clearInterval(state.keepTimer);
state.keepTimer = null;
if (ws.state() === WebSocket.OPEN) ws.close();
clearInterval(taskState.keepTimer);
wk.send({ type: 'close' }) && wk.close();
if (ws.state() <= WebSocket.OPEN) ws.close();
});
</script>
<template>
<PageContainer>
<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
type="primary"
:disabled="state.initialized"
@click="fnStart"
:disabled="!taskState.stop && taskState.task.taskNo !== ''"
>
<PlayCircleOutlined />
Start Trace
<template #overlay>
<a-menu
@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">
<template #content>
<div v-for="c in v.children">{{ c.id }}</div>
@@ -366,79 +606,130 @@ onBeforeUnmount(() => {
<template #icon><DownOutlined /></template>
</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>
Stop Trace
</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
type="primary"
:loading="downLoading"
@click.prevent="fnDownloadPCAP()"
v-if="state.task.outputPCAP"
v-if="taskState.stop && taskState.stopOutputPCAP"
>
<template #icon><DownloadOutlined /></template>
{{ t('common.downloadText') }}
</a-button>
<a-tag
color="green"
v-show="!!state.task.filter && state.initialized"
>
{{ state.task.filter }}
</a-tag>
</a-space>
<a-space :size="8" class="toolbar-info" v-show="state.initialized">
<span>
{{ t('views.traceManage.task.traceId') }}:&nbsp;
<strong>{{ state.task.taskNo }}</strong>
Packets:
<strong>{{ tableState.total }}</strong>
</span>
<span>
Task No:
<strong>{{ taskState.task.taskNo }}</strong>
</span>
<span> Packets: {{ state.totalPackets }} </span>
</a-space>
</div>
</template>
<!-- 包数据表过滤 -->
<a-input-group compact v-show="state.initialized">
<a-input
v-model:value="state.filter"
placeholder="display filter, example: tcp"
:allow-clear="true"
<a-input-group compact>
<a-auto-complete
v-model:value="taskState.filter"
:options="[
{ 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)"
@pressEnter="handleFilterFrames"
:allow-clear="true"
>
<template #prefix>
<FilterOutlined />
</template>
</a-input>
<a-input
placeholder="BPF Basic Filter, example: tcp"
@pressEnter="handleFilterFrames"
>
<template #prefix>
<FilterOutlined />
</template>
</a-input>
</a-auto-complete>
<a-button
type="primary"
html-type="submit"
style="width: 100px"
@click="handleFilterFrames"
:disabled="taskState.task.taskNo === '' || taskState.stop"
>
Filter
</a-button>
</a-input-group>
<a-alert
:message="state.filterError"
:message="taskState.filterError"
type="error"
v-if="state.filterError != null"
v-if="taskState.filterError != null"
/>
<!-- 包数据表 -->
<PacketTable
:columns="state.columns"
:data="state.packetList"
:selectedFrame="state.selectedFrame"
:onSelectedFrame="handleSelectedFrame"
:onScrollBottom="handleScrollBottom"
></PacketTable>
<a-row>
<a-table
class="table"
row-key="number"
:columns="tableColumns"
:loading="tableState.loading"
:data-source="tableState.data"
size="small"
:pagination="false"
: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">
<!-- 帧数据 -->
<DissectionTree
id="root"
:select="handleSelectedTreeEntry"
:select="handleSelectedTree"
:selected="state.selectedTree"
:tree="state.packetFrame.tree"
/>
@@ -474,28 +765,6 @@ onBeforeUnmount(() => {
</template>
<style scoped>
.toolbar {
display: flex;
align-items: center;
margin-bottom: 12px;
}
.toolbar-oper {
flex: 1;
}
.toolbar-info {
text-align: right;
padding-right: 8px;
}
.summary {
display: flex;
flex-direction: column;
}
.summary-item > span:first-child {
font-weight: 600;
margin-right: 6px;
}
.tree {
font-size: 0.8125rem;
line-height: 1.5rem;