Merge remote-tracking branch 'origin/lichang'

This commit is contained in:
TsMask
2024-10-17 18:26:49 +08:00
28 changed files with 1544 additions and 538 deletions

View File

@@ -30,6 +30,7 @@
"dayjs": "^1.11.11", "dayjs": "^1.11.11",
"echarts": "~5.5.0", "echarts": "~5.5.0",
"file-saver": "^2.0.5", "file-saver": "^2.0.5",
"grid-layout-plus": "^1.0.5",
"intl-tel-input": "^23.8.1", "intl-tel-input": "^23.8.1",
"js-base64": "^3.7.7", "js-base64": "^3.7.7",
"js-cookie": "^3.0.5", "js-cookie": "^3.0.5",
@@ -41,7 +42,6 @@
"vue-i18n": "^9.13.1", "vue-i18n": "^9.13.1",
"vue-router": "^4.4.0", "vue-router": "^4.4.0",
"vue3-smooth-dnd": "^0.0.6", "vue3-smooth-dnd": "^0.0.6",
"vuedraggable": "^4.1.0",
"xlsx": "~0.18.5" "xlsx": "~0.18.5"
}, },
"devDependencies": { "devDependencies": {

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

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

View File

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

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

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

View File

@@ -20,17 +20,6 @@ export function dumpStop(data: Record<string, string>) {
}); });
} }
// 网元抓包PACP 下载
export function dumpDownload(data: Record<string, any>) {
return request({
url: '/trace/tcpdump/download',
method: 'get',
params: data,
responseType: 'blob',
timeout: 60_000,
});
}
// UPF标准版内部抓包 // UPF标准版内部抓包
export function traceUPF(data: Record<string, string>) { export function traceUPF(data: Record<string, string>) {
return request({ return request({

View File

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

View File

@@ -685,11 +685,12 @@ export default {
addrPlease: "Please fill in the host IP address correctly", addrPlease: "Please fill in the host IP address correctly",
port: "Port", port: "Port",
portPlease: "Please fill in the host port number correctly", portPlease: "Please fill in the host port number correctly",
user: "Login User", user: "User",
userPlease: "Please fill in the host login user correctly", userPlease: "Please fill in the host user correctly",
database: "DataBase",
authMode: "Auth Mode", authMode: "Auth Mode",
password: "Password", password: "Password",
passwordPlease: "Please fill in the host login password correctly", passwordPlease: "Please fill in the host password correctly",
privateKey: "Private Key", privateKey: "Private Key",
privateKeyPlease: "Please fill in the private key characters correctly ~/.ssh/id_rsa", privateKeyPlease: "Please fill in the private key characters correctly ~/.ssh/id_rsa",
passPhrase: "Private Key Cipher", passPhrase: "Private Key Cipher",
@@ -1110,8 +1111,8 @@ export default {
fileUPFTip: 'UPF internal packet capture and analysis packet', fileUPFTip: 'UPF internal packet capture and analysis packet',
textStart: "Start", textStart: "Start",
textStop: "Stop", textStop: "Stop",
textLog: "Log", textLog: "LogFile",
textLogMsg: "Log Info", textLogMsg: "LogFile Info",
textDown: "Download", textDown: "Download",
downTip: "Are you sure you want to download the {title} capture data file?", downTip: "Are you sure you want to download the {title} capture data file?",
downOk: "{title} file download complete", downOk: "{title} file download complete",

View File

@@ -686,10 +686,11 @@ export default {
port: "端口", port: "端口",
portPlease: "请正确填写主机端口号", portPlease: "请正确填写主机端口号",
user: "用户名", user: "用户名",
userPlease: "请正确填写主机登录用户", userPlease: "请正确填写主机用户",
database: "数据库",
authMode: "认证模式", authMode: "认证模式",
password: "密码", password: "密码",
passwordPlease: "请正确填写主机登录密码", passwordPlease: "请正确填写主机密码",
privateKey: "私钥", privateKey: "私钥",
privateKeyPlease: "请正确填写私钥字符内容 ~/.ssh/id_rsa", privateKeyPlease: "请正确填写私钥字符内容 ~/.ssh/id_rsa",
passPhrase: "私钥密码", passPhrase: "私钥密码",
@@ -1110,8 +1111,8 @@ export default {
fileUPFTip: 'UPF内部抓包分析包', fileUPFTip: 'UPF内部抓包分析包',
textStart: "开始", textStart: "开始",
textStop: "停止", textStop: "停止",
textLog: "日志", textLog: "日志文件",
textLogMsg: "日志信息", textLogMsg: "日志文件信息",
textDown: "下载", textDown: "下载",
downTip: "确认要下载 {title} 抓包数据文件吗?", downTip: "确认要下载 {title} 抓包数据文件吗?",
downOk: "{title} 文件下载完成", downOk: "{title} 文件下载完成",

View File

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

View File

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

View File

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

View File

@@ -44,7 +44,7 @@ export default function useWS() {
// 普通信息 // 普通信息
switch (requestId) { switch (requestId) {
// AMF_UE会话事件 // AMF_UE会话事件
case 'amf_1010_001': case 'amf_1010':
if (Array.isArray(data.rows)) { if (Array.isArray(data.rows)) {
eventListParse('amf_ue', data); eventListParse('amf_ue', data);
} }
@@ -85,7 +85,7 @@ export default function useWS() {
} }
break; break;
// AMF_UE会话事件 // AMF_UE会话事件
case '1010_001': case '1010':
if (data.data) { if (data.data) {
queue.add(() => eventItemParseAndPush('amf_ue', data.data)); queue.add(() => eventItemParseAndPush('amf_ue', data.data));
} }
@@ -128,7 +128,7 @@ export default function useWS() {
function userActivitySend() { function userActivitySend() {
// AMF_UE会话事件 // AMF_UE会话事件
ws.send({ ws.send({
requestId: 'amf_1010_001', requestId: 'amf_1010',
type: 'amf_ue', type: 'amf_ue',
data: { data: {
neType: 'AMF', neType: 'AMF',
@@ -175,11 +175,11 @@ export default function useWS() {
/**订阅通道组 /**订阅通道组
* *
* 指标UPF (GroupID:12_neId) * 指标UPF (GroupID:12_neId)
* AMF_UE会话事件(GroupID:1010_neId) * AMF_UE会话事件(GroupID:1010)
* MME_UE会话事件(GroupID:1011_neId) * MME_UE会话事件(GroupID:1011_neId)
* IMS_CDR会话事件(GroupID:1005_neId) * IMS_CDR会话事件(GroupID:1005_neId)
*/ */
subGroupID: '12_001,1010_001,1011_001,1005_001', subGroupID: '12_001,1010,1011_001,1005_001',
}, },
onmessage: wsMessage, onmessage: wsMessage,
onerror: (ev: any) => { onerror: (ev: any) => {

View File

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

View File

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

View File

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

View File

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

View File

@@ -517,7 +517,7 @@ function fnExportList(type: string) {
if (!neId) return; if (!neId) return;
const hide = message.loading(t('common.loading'), 0); const hide = message.loading(t('common.loading'), 0);
exportUDMAuth({ ...queryParams, ...{ type } }) exportUDMAuth(Object.assign({ type: type }, queryParams))
.then(res => { .then(res => {
if (res.code === RESULT_CODE_SUCCESS) { if (res.code === RESULT_CODE_SUCCESS) {
message.success(t('common.msgSuccess', { msg: t('common.export') }), 3); message.success(t('common.msgSuccess', { msg: t('common.export') }), 3);
@@ -555,6 +555,9 @@ function fnLoadData() {
fnQueryReset(); fnQueryReset();
}, timerS * 1000); }, timerS * 1000);
} else { } else {
modalState.loadDataLoading = false;
tableState.loading = false; // 表格loading
fnQueryReset();
message.error({ message.error({
content: t('common.getInfoFail'), content: t('common.getInfoFail'),
duration: 3, duration: 3,

View File

@@ -878,7 +878,7 @@ function fnExportList(type: string) {
if (!neId) return; if (!neId) return;
const key = 'exportSub'; const key = 'exportSub';
message.loading({ content: t('common.loading'), key }); message.loading({ content: t('common.loading'), key });
exportUDMSub({ ...queryParams, ...{ type } }).then(res => { exportUDMSub(Object.assign({ type: type }, queryParams)).then(res => {
if (res.code === RESULT_CODE_SUCCESS) { if (res.code === RESULT_CODE_SUCCESS) {
message.success({ message.success({
content: t('common.msgSuccess', { msg: t('common.export') }), content: t('common.msgSuccess', { msg: t('common.export') }),
@@ -920,6 +920,9 @@ function fnLoadData() {
fnQueryReset(); fnQueryReset();
}, timerS * 1000); }, timerS * 1000);
} else { } else {
modalState.loadDataLoading = false;
tableState.loading = false; // 表格loading
fnQueryReset();
message.error({ message.error({
content: t('common.getInfoFail'), content: t('common.getInfoFail'),
duration: 3, duration: 3,

View File

@@ -1,13 +1,10 @@
<script setup lang="ts"> <script setup lang="ts">
import draggable from 'vuedraggable'; import { DragOutlined } from '@ant-design/icons-vue';
import { GridLayout, GridItem } from 'grid-layout-plus'
import * as echarts from 'echarts'; import * as echarts from 'echarts';
import { PageContainer } from 'antdv-pro-layout'; import { PageContainer } from 'antdv-pro-layout';
import { onMounted, reactive, ref, markRaw, nextTick, onUnmounted } from 'vue'; import { onMounted, reactive, ref, markRaw, nextTick, onUnmounted, watch } from 'vue';
import { import { RESULT_CODE_ERROR, RESULT_CODE_SUCCESS } from '@/constants/result-constants';
RESULT_CODE_ERROR,
RESULT_CODE_SUCCESS,
} from '@/constants/result-constants';
import { SizeType } from 'ant-design-vue/es/config-provider'; import { SizeType } from 'ant-design-vue/es/config-provider';
import { listKPIData, getKPITitle } from '@/api/perfManage/goldTarget'; import { listKPIData, getKPITitle } from '@/api/perfManage/goldTarget';
import useI18n from '@/hooks/useI18n'; import useI18n from '@/hooks/useI18n';
@@ -19,26 +16,160 @@ import { ColumnsType } from 'ant-design-vue/es/table';
import { generateColorRGBA } from '@/utils/generate-utils'; import { generateColorRGBA } from '@/utils/generate-utils';
import { LineSeriesOption } from 'echarts/charts'; import { LineSeriesOption } from 'echarts/charts';
import { OptionsType, WS } from '@/plugins/ws-websocket'; import { OptionsType, WS } from '@/plugins/ws-websocket';
import { Switch } from 'ant-design-vue'; import { Select } from 'ant-design-vue';
const { t, currentLocale } = useI18n(); const { t, currentLocale } = useI18n();
const neInfoStore = useNeInfoStore(); const neInfoStore = useNeInfoStore();
//WebSocket连接 //WebSocket连接
//const ws = new WS();
const ws = ref<WS | null>(null); const ws = ref<WS | null>(null);
//实时数据开关
const handleRealTimeSwitch = (checked: any, event: Event) => {
console.log('Switch toggled:', checked);
fnRealTimeSwitch(!!checked);
};
//添加实时数据开关状态 //添加实时数据开关状态
const realTimeEnabled = ref(false); const realTimeEnabled = ref(false);
//实时数据开关
const handleRealTimeSwitch = (checked: any) => {
fnRealTimeSwitch(!!checked);
};
// 定义所有可能的网元类型
const ALL_NE_TYPES = ['ims', 'amf', 'udm', 'smf', 'pcf','upf','mme','mocngw','smsc','cbc','ausf'] as const;
type AllChartType = typeof ALL_NE_TYPES[number];
// 使用 ref 来使 networkElementTypes 变为响应式,并使用 ALL_NE_TYPES 初始化
const networkElementTypes = ref<AllChartType[]>([...ALL_NE_TYPES]);
// 添加选择的网元类型,也使用 ALL_NE_TYPES 初始化
const selectedNeTypes = ref<AllChartType[]>([...ALL_NE_TYPES]);
// 监听 selectedNeTypes 的变化
watch(selectedNeTypes, (newTypes) => {
//console.log('Selected types changed:', newTypes);
if (JSON.stringify(newTypes) !== JSON.stringify(networkElementTypes.value)) {
networkElementTypes.value = newTypes;
// 更新 chartOrder
chartOrder.value = chartOrder.value.filter(item => newTypes.includes(item.i));
newTypes.forEach((type) => {
if (!chartOrder.value.some(item => item.i === type)) {
chartOrder.value.push({
x: (chartOrder.value.length % 2) * 6,
y: Math.floor(chartOrder.value.length / 2) * 4,
w: 6,
h: 4,
i: type,
});
}
// 确保 chartStates 包含新的网元类型
if (!chartStates[type]) {
chartStates[type] = createChartState();
}
});
//console.log('Updated chartOrder:', chartOrder.value);
// 保存选中的网元类型到本地存储
localStorage.setItem('selectedNeTypes', JSON.stringify(newTypes));
// 重新初始化图表
nextTick(() => {
initCharts();
});
}
}, { deep: true });
// 初始化所有图表的函数
const initCharts = async () => {
//console.log('Initializing charts for:', networkElementTypes.value);
// 清除不再需要的图表
Object.keys(chartStates).forEach((key) => {
if (!networkElementTypes.value.includes(key as AllChartType)) {
const state = chartStates[key as AllChartType];
if (state.chart.value) {
state.chart.value.dispose();
}
if (state.observer.value) {
state.observer.value.disconnect();
}
delete chartStates[key as AllChartType];
}
});
// 初始化或更新需要的图表
for (const type of networkElementTypes.value) {
//console.log('Initializing chart for:', type);
if (!chartStates[type]) {
chartStates[type] = createChartState();
}
try {
await fetchKPITitle(type);
await nextTick();
initChart(type);
await fetchData(type);
} catch (error) {
console.error(`Error initializing chart for ${type}:`, error);
}
}
//console.log('Finished initializing charts');
// 保存更新后的布局
localStorage.setItem('chartOrder', JSON.stringify(chartOrder.value));
};
// 添加类型定义
interface LayoutItem {
x: number;
y: number;
w: number;
h: number;
i: AllChartType; // 将 ChartType 改为 AllChartType
}
type Layout = LayoutItem[];
// 定义图表类型
type ChartType = 'udm' | 'upf' | 'amf' | 'smf';
//构建响应式数组储存图表类型数据 //构建响应式数组储存图表类型数据
const chartOrder = ref(['udm', 'upf', 'amf', 'smf']); const chartOrder = ref<Layout>(
JSON.parse(localStorage.getItem('chartOrder') || 'null') ||
networkElementTypes.value.map((type, index) => ({
x: index % 2 * 6, // 每行两个图表宽度为6
y: Math.floor(index / 2) * 4, // 每个图表占据 4 个单位高度
w: 6, // 宽度为6单位
h: 4, // 高度为4个单位
i: type, // 使用网元类型作为唯一标识符
}))
);
// 改变布局触发更新
const handleLayoutUpdated = (newLayout: Layout) => {
const filteredLayout = newLayout.filter(item => networkElementTypes.value.includes(item.i));
if (JSON.stringify(filteredLayout) !== JSON.stringify(chartOrder.value)) {
chartOrder.value = filteredLayout;
// 保存布局到 localStorage
localStorage.setItem('chartOrder', JSON.stringify(chartOrder.value));
nextTick(() => {
chartOrder.value.forEach((item) => {
const state = chartStates[item.i];
if (state?.chart.value) {
state.chart.value.resize();
}
});
});
}
};
// 监听 chartOrder 的变化
watch(chartOrder, () => {
nextTick(() => {
Object.values(chartStates).forEach(state => {
if (state.chart.value) {
state.chart.value.resize();
}
});
});
});
// 定义表格状态类型 // 定义表格状态类型
type TableStateType = { type TableStateType = {
@@ -49,10 +180,17 @@ type TableStateType = {
selectedRowKeys: (string | number)[]; selectedRowKeys: (string | number)[];
}; };
// 创建可复用的状态 // 创建可复用的状态
const createChartState = () => ({ const createChartState = () => {
chartDom: ref<HTMLElement | null>(null), const chartDom = ref<HTMLElement | null>(null);
chart: ref<echarts.ECharts | null>(null), const chart = ref<echarts.ECharts | null>(null);
const observer = ref<ResizeObserver | null>(null);
return {
chartDom,
chart,
observer,
tableColumns: ref<ColumnsType>([]), tableColumns: ref<ColumnsType>([]),
tableState: reactive<TableStateType>({ tableState: reactive<TableStateType>({
loading: false, loading: false,
@@ -64,72 +202,67 @@ const createChartState = () => ({
chartLegendSelected: {} as Record<string, boolean>, chartLegendSelected: {} as Record<string, boolean>,
chartDataXAxisData: [] as string[], chartDataXAxisData: [] as string[],
chartDataYSeriesData: [] as CustomSeriesOption[], chartDataYSeriesData: [] as CustomSeriesOption[],
}); };
// 为每种图表类型创建状态
const chartStates: Record<ChartType, ReturnType<typeof createChartState>> = {
udm: createChartState(),
upf: createChartState(),
amf: createChartState(),
smf: createChartState(),
}; };
// 为每种图表类型创建状
const chartStates: Record<AllChartType, ReturnType<typeof createChartState>> = Object.fromEntries(
networkElementTypes.value.map(type => [type, createChartState()])
) as Record<AllChartType, ReturnType<typeof createChartState>>;
//日期选择器 //日期选择器
interface RangePicker { interface RangePicker extends Record<AllChartType, [string, string]> {
udm: [string, string];
upf: [string, string];
amf: [string, string];
smf: [string, string];
placeholder: [string, string]; placeholder: [string, string];
ranges: Record<string, [Dayjs, Dayjs]>; ranges: Record<string, [Dayjs, Dayjs]>;
} }
// 创建日期选择器状态 // 创建日期选择器状态
const rangePicker = reactive<RangePicker>({ const rangePicker = reactive<RangePicker>({
udm: [ ...Object.fromEntries(networkElementTypes.value.map(type => [
dayjs('2024-09-20 00:00:00').valueOf().toString(), type,
dayjs('2024-09-20 23:59:59').valueOf().toString(), // [
], // dayjs('2024-09-20 00:00:00').valueOf().toString(),//拟数据的日期设2024.9.20
upf: [ // dayjs('2024-09-20 23:59:59').valueOf().toString()
dayjs('2024-09-20 00:00:00').valueOf().toString(), // ]
dayjs('2024-09-20 23:59:59').valueOf().toString(), [
], dayjs().startOf('day').valueOf().toString(), // 当天 0 点 0 分 0 秒
amf: [ dayjs().valueOf().toString() // 当前时间
dayjs('2024-09-20 00:00:00').valueOf().toString(), ]
dayjs('2024-09-20 23:59:59').valueOf().toString(), ])) as Record<AllChartType, [string, string]>,
], placeholder: [t('views.monitor.monitor.startTime'), t('views.monitor.monitor.endTime')] as [string, string],
smf: [
dayjs('2024-09-20 00:00:00').valueOf().toString(),
dayjs('2024-09-20 23:59:59').valueOf().toString(),
],
placeholder: [
t('views.monitor.monitor.startTime'),
t('views.monitor.monitor.endTime'),
] as [string, string],
ranges: { ranges: {
[t('views.monitor.monitor.yesterday')]: [ [t('views.monitor.monitor.yesterday')]: [dayjs().subtract(1, 'day').startOf('day'), dayjs().subtract(1, 'day').endOf('day')],
dayjs().subtract(1, 'day').startOf('day'),
dayjs().subtract(1, 'day').endOf('day'),
],
[t('views.monitor.monitor.today')]: [dayjs().startOf('day'), dayjs()], [t('views.monitor.monitor.today')]: [dayjs().startOf('day'), dayjs()],
[t('views.monitor.monitor.week')]: [ [t('views.monitor.monitor.week')]: [dayjs().startOf('week'), dayjs().endOf('week')],
dayjs().startOf('week'), [t('views.monitor.monitor.month')]: [dayjs().startOf('month'), dayjs().endOf('month')],
dayjs().endOf('week'), } as Record<string, [Dayjs, Dayjs]>,
],
[t('views.monitor.monitor.month')]: [
dayjs().startOf('month'),
dayjs().endOf('month'),
],
},
}); });
// 创建可复用的图表初始化函数 // 创建可复用的图表初始化函数
const initChart = (type: ChartType) => { const initChart = (type: AllChartType) => {
const tryInit = (retries = 3) => {
nextTick(() => { nextTick(() => {
const state = chartStates[type]; const state = chartStates[type];
if (!state) {
console.warn(`Chart state for ${type} not found`);
return;
}
const container = state.chartDom.value; const container = state.chartDom.value;
if (!container) return; if (!container) {
state.chart.value = markRaw(echarts.init(container, 'light')); if (retries > 0) {
console.warn(`Chart container for ${type} not found, retrying... (${retries} attempts left)`);
setTimeout(() => tryInit(retries - 1), 100);
} else {
console.error(`Chart container for ${type} not found after multiple attempts`);
}
return;
}
if (state.chart.value) {
state.chart.value.dispose();
}
state.chart.value = markRaw(echarts.init(container));
const option: echarts.EChartsOption = { const option: echarts.EChartsOption = {
tooltip: { tooltip: {
trigger: 'axis', trigger: 'axis',
@@ -176,29 +309,36 @@ const initChart = (type: ChartType) => {
series: [], series: [],
}; };
state.chart.value.setOption(option); state.chart.value.setOption(option);
}); state.chart.value.resize(); // 确保图表正确调整大小
};
//结束拖拽事件 // 创建 ResizeObserver 实例
const onDragEnd = () => { if (state.observer.value) {
nextTick(() => { state.observer.value.disconnect();
chartOrder.value.forEach(type => { }
const state = chartStates[type as ChartType]; state.observer.value = new ResizeObserver(() => {
if (state.chart.value) { if (state.chart.value) {
state.chart.value.dispose(); // 销毁旧的图表实例 state.chart.value.resize();
} }
initChart(type as ChartType);
fetchData(type as ChartType); // 重新获取数据并渲染图表
}); });
// 开始观察图表容器
state.observer.value.observe(container);
}); });
}; };
// 创建可复用的数据获取函数 tryInit();
const fetchData = async (type: ChartType) => { };
const state = chartStates[type];
// 可复用的数据获函数
const fetchData = async (type: AllChartType) => {
const state = chartStates[type]; // 直接使用 type
const neId = '001'; const neId = '001';
state.tableState.loading = true; state.tableState.loading = true;
try { try {
const [startTime, endTime] = rangePicker[type]; const dateRange = rangePicker[type] as [string, string];
const [startTime, endTime] = dateRange;
const res = await listKPIData({ const res = await listKPIData({
neType: type.toUpperCase(), neType: type.toUpperCase(),
neId, neId,
@@ -210,11 +350,12 @@ const fetchData = async (type: ChartType) => {
}); });
if (res.code === RESULT_CODE_SUCCESS && Array.isArray(res.data)) { if (res.code === RESULT_CODE_SUCCESS && Array.isArray(res.data)) {
state.tableState.data = res.data; state.tableState.data = res.data;
nextTick(() => { await nextTick(() => {
renderChart(type); renderChart(type);
}); });
} }
} catch (error) { } catch (error) {
console.log("123")
console.error(error); console.error(error);
message.error(t('common.getInfoFail')); message.error(t('common.getInfoFail'));
} finally { } finally {
@@ -224,11 +365,11 @@ const fetchData = async (type: ChartType) => {
//建立实时数据连接 //建立实时数据连接
function fnRealTimeSwitch(bool: boolean) { function fnRealTimeSwitch(bool: boolean) {
console.log('fnRealTimeSwitch called with:', bool);
realTimeEnabled.value = bool; realTimeEnabled.value = bool;
if (bool) { if (bool) {
if(!ws.value){ if(!ws.value){
console.log('Creating new WS instance');
ws.value = new WS(); ws.value = new WS();
} }
Object.values(chartStates).forEach(state => { Object.values(chartStates).forEach(state => {
@@ -238,21 +379,15 @@ function fnRealTimeSwitch(bool: boolean) {
const options: OptionsType = { const options: OptionsType = {
url: '/ws', url: '/ws',
params: { params: {
subGroupID: Object.keys(chartStates) subGroupID: networkElementTypes.value.map(type => `10_${type.toUpperCase()}_001`).join(','),
.map(type => `10_${type.toUpperCase()}_001`)
.join(','),
}, },
onmessage: wsMessage, onmessage: wsMessage,
onerror: wsError, onerror: wsError,
onopen: () => {
console.log('WebSocket connection established');
},
}; };
console.log('Attempting to connect with options:', options);
ws.value.connect(options); ws.value.connect(options);
console.log('Connection attempt initiated');
} else if(ws.value){ } else if(ws.value){
console.log('Closing WebSocket connection');
Object.values(chartStates).forEach(state => { Object.values(chartStates).forEach(state => {
state.tableState.seached = true; state.tableState.seached = true;
}); });
@@ -261,58 +396,61 @@ function fnRealTimeSwitch(bool: boolean) {
} }
} }
// 接收数据后错误回 // 接收数据后错误回
function wsError(ev: any) { function wsError() {
console.error('WebSocket error:', ev);
message.error(t('common.websocketError')); message.error(t('common.websocketError'));
} }
// 接收数据后回调 // 修改 wsMessage 数
function wsMessage(res: Record<string, any>) { function wsMessage(res: Record<string, any>) {
//const res = JSON.parse(event.data);
const { code, data } = res; const { code, data } = res;
if (code === RESULT_CODE_ERROR) { if (code === RESULT_CODE_ERROR) {
console.warn(res.msg); console.warn(res.msg);
return; return;
} }
// 订阅组信息
if (!data?.groupId) { if (!data?.groupId) {
return; return;
} }
// 处理四个图表的数据 networkElementTypes.value.forEach((type) => {
(Object.keys(chartStates) as ChartType[]).forEach(type => {
const state = chartStates[type]; const state = chartStates[type];
const kpiEvent = data.data[type.toUpperCase()]; const kpiEvent:any = data.data[type.toUpperCase()];
if (kpiEvent) { if (kpiEvent) {
// 更新 X 轴数据
if (kpiEvent.timeGroup) { if (kpiEvent.timeGroup) {
state.chartDataXAxisData.push(parseDateToStr(+kpiEvent.timeGroup)); const newTime = parseDateToStr(+kpiEvent.timeGroup);
state.chartDataXAxisData.push(newTime);
if (state.chartDataXAxisData.length > 100) { if (state.chartDataXAxisData.length > 100) {
state.chartDataXAxisData.shift(); state.chartDataXAxisData.shift();
} }
}
// 更新 Y 轴数据 // 使用 appendData 方法追加数据
state.chartDataYSeriesData.forEach(series => { state.chartDataYSeriesData.forEach(series => {
if (kpiEvent[series.customKey as string] !== undefined) { if (kpiEvent[series.customKey as string] !== undefined) {
series.data.push(+kpiEvent[series.customKey as string]); const newValue = +kpiEvent[series.customKey as string];
if (state.chart.value) {
state.chart.value.appendData({
seriesIndex: state.chartDataYSeriesData.indexOf(series),
data: [[newTime, newValue]]
});
}
// 保持数据长度不超过100
if (series.data.length > 100) { if (series.data.length > 100) {
series.data.shift(); series.data.shift();
} }
} }
}); });
// 更新图表 // 更新 X 轴
if (state.chart.value) { if (state.chart.value) {
state.chart.value.setOption({ state.chart.value.setOption({
xAxis: { data: state.chartDataXAxisData }, xAxis: { data: state.chartDataXAxisData }
series: state.chartDataYSeriesData,
}); });
} }
} }
}
}); });
} }
@@ -325,26 +463,23 @@ interface CustomSeriesOption extends Omit<LineSeriesOption, 'data'> {
data: (number | LineDataItem)[]; data: (number | LineDataItem)[];
} }
// 创建可复用的图表渲染函数 // 创建可复用的图表渲染函数
const renderChart = (type: ChartType) => { const renderChart = (type: AllChartType) => {
const state = chartStates[type]; const state = chartStates[type];
if (state.chart.value == null || state.tableState.data.length <= 0) { if (state.chart.value == null) {
return; return;
} }
// 重置数据 // 重置数据
state.chartLegendSelected = {}; state.chartLegendSelected = {};
state.chartDataXAxisData = []; state.chartDataXAxisData = [];
state.chartDataYSeriesData = []; state.chartDataYSeriesData = [];
// 处理数据 // 处理数据
for (const columns of state.tableColumns.value) { for (const column of state.tableColumns.value) {
if (['neName', 'startIndex', 'timeGroup'].includes(columns.key as string)) if (['neName', 'startIndex', 'timeGroup'].includes(column.key as string)) continue;
continue;
const color = generateColorRGBA(); const color = generateColorRGBA();
state.chartDataYSeriesData.push({ state.chartDataYSeriesData.push({
name: columns.title as string, name: column.title as string,
customKey: columns.key as string, customKey: column.key as string,
//key: columns.key as string,
type: 'line', type: 'line',
symbol: 'none', symbol: 'none',
sampling: 'lttb', sampling: 'lttb',
@@ -357,17 +492,14 @@ const renderChart = (type: ChartType) => {
}, },
data: [], data: [],
} as CustomSeriesOption); } as CustomSeriesOption);
state.chartLegendSelected[columns.title as string] = true; state.chartLegendSelected[column.title as string] = true;
//});
//state.chartLegendSelected[columns.title as string] = true;
} }
const orgData = [...state.tableState.data].reverse(); const orgData = [...state.tableState.data].reverse();
for (const item of orgData) { for (const item of orgData) {
state.chartDataXAxisData.push(parseDateToStr(+item.timeGroup)); state.chartDataXAxisData.push(parseDateToStr(+item.timeGroup));
for (const y of state.chartDataYSeriesData) { for (const series of state.chartDataYSeriesData) {
//const key = (y.emphasis as any).customKey; series.data.push(+item[series.customKey as string]);
y.data.push(+item[y.customKey as string]);
} }
} }
@@ -375,7 +507,11 @@ const renderChart = (type: ChartType) => {
state.chart.value.setOption( state.chart.value.setOption(
{ {
legend: { selected: state.chartLegendSelected }, legend: { selected: state.chartLegendSelected },
xAxis: { data: state.chartDataXAxisData }, xAxis: {
data: state.chartDataXAxisData,
type: 'category',
boundaryGap: false,
},
series: state.chartDataYSeriesData, series: state.chartDataYSeriesData,
dataZoom: [ dataZoom: [
{ {
@@ -393,14 +529,13 @@ const renderChart = (type: ChartType) => {
); );
}; };
// 获取表头数据
const fetchKPITitle = async (type: ChartType) => { // 获取头数据
const language = const fetchKPITitle = async (type: AllChartType) => {
currentLocale.value.split('_')[0] === 'zh' const language = currentLocale.value.split('_')[0] === 'zh' ? 'cn' : currentLocale.value.split('_')[0];
? 'cn'
: currentLocale.value.split('_')[0];
try { try {
const res = await getKPITitle(type.toUpperCase()); const res = await getKPITitle(type.toUpperCase());
console.log(res);
if (res.code === RESULT_CODE_SUCCESS && Array.isArray(res.data)) { if (res.code === RESULT_CODE_SUCCESS && Array.isArray(res.data)) {
chartStates[type].tableColumns.value = res.data.map(item => ({ chartStates[type].tableColumns.value = res.data.map(item => ({
title: item[`${language}Title`], title: item[`${language}Title`],
@@ -414,20 +549,57 @@ const fetchKPITitle = async (type: ChartType) => {
})); }));
} }
} catch (error) { } catch (error) {
console.log("321")
console.error(error); console.error(error);
message.warning(t('common.getInfoFail')); message.warning(t('common.getInfoFail'));
} }
}; };
// 初始化所有图表 // 定义默认选择的网元类型
const DEFAULT_NE_TYPES: AllChartType[] = ['udm', 'amf', 'upf', 'ims'];
// 在 onMounted 钩子中
onMounted(async () => { onMounted(async () => {
ws.value = new WS(); ws.value = new WS();
await neInfoStore.fnNelist(); await neInfoStore.fnNelist();
for (const type of Object.keys(chartStates) as ChartType[]) {
await fetchKPITitle(type); // 从本地存储中读取选中的网元类型
initChart(type); const savedSelectedNeTypes = localStorage.getItem('selectedNeTypes');
fetchData(type); if (savedSelectedNeTypes) {
const parsedSelectedNeTypes = JSON.parse(savedSelectedNeTypes) as AllChartType[];
selectedNeTypes.value = parsedSelectedNeTypes;
networkElementTypes.value = parsedSelectedNeTypes;
} else {
// 如果没有保存的选中网元类型,则使用默认选择
selectedNeTypes.value = [...DEFAULT_NE_TYPES];
networkElementTypes.value = [...DEFAULT_NE_TYPES];
// 保存这个默认选择到本地存储
localStorage.setItem('selectedNeTypes', JSON.stringify(DEFAULT_NE_TYPES));
} }
// 初始化或更新 chartOrder
const savedLayout = localStorage.getItem('chartOrder');
if (savedLayout) {
const parsedLayout = JSON.parse(savedLayout);
// 只保留当前选中的网元类型的布局
chartOrder.value = parsedLayout.filter((item: LayoutItem) => networkElementTypes.value.includes(item.i));
}
// 如果 chartOrder 为空或者不包含所有选中的网元,重新创建布局
if (chartOrder.value.length === 0 || chartOrder.value.length !== networkElementTypes.value.length) {
chartOrder.value = networkElementTypes.value.map((type, index) => ({
x: index % 2 * 6,
y: Math.floor(index / 2) * 4,
w: 6,
h: 4,
i: type,
}));
}
//console.log('Initialized networkElementTypes:', networkElementTypes.value);
//console.log('Initialized chartOrder:', chartOrder.value);
await initCharts();
}); });
// 在组件卸载时销毁图表实例 // 在组件卸载时销毁图表实例
@@ -435,45 +607,86 @@ onUnmounted(() => {
if(ws.value &&ws.value.state()===WebSocket.OPEN) { if(ws.value &&ws.value.state()===WebSocket.OPEN) {
ws.value.close(); ws.value.close();
} }
Object.values(chartStates).forEach(state => { Object.values(chartStates).forEach((state) => {
if (state.chart.value) { if (state.chart.value) {
state.chart.value.dispose(); state.chart.value.dispose();
} }
if (state.observer.value) {
state.observer.value.disconnect();
}
}); });
}); });
</script> </script>
<template> <template>
<PageContainer> <PageContainer>
<div class="control-row"> <a-card :bordered="false" class="control-card">
<div class="real-time-switch"> <a-form layout="inline">
<Switch <a-form-item>
<a-switch
v-model:checked="realTimeEnabled" v-model:checked="realTimeEnabled"
@change="handleRealTimeSwitch as any" @change="handleRealTimeSwitch"
/> />
<span class="switch-label">{{ </a-form-item>
realTimeEnabled ? '实时数据已开启' : '实时数据已关闭' <a-form-item>
}}</span> <span class="switch-label">{{ realTimeEnabled ? t('views.dashboard.cdr.realTimeDataStart') : t('views.dashboard.cdr.realTimeDataStop') }}</span>
</div> </a-form-item>
</div> <a-form-item :label="t('views.ne.common.neType')" class="ne-type-select">
<draggable <a-select
v-model="chartOrder" v-model:value="selectedNeTypes"
:animation="200" mode="multiple"
item-key="type" style="min-width: 200px; width: 100%"
@end="onDragEnd" :placeholder="t('common.selectPlease')"
class="row"
onscroll="false"
> >
<template #item="{ element: type }"> <a-select-option v-for="type in ALL_NE_TYPES" :key="type" :value="type">
<div class="col-lg-6 col-md=-6 col-xs-12"> {{ type.toUpperCase() }}
<a-card </a-select-option>
:bordered="false" </a-select>
:body-style="{ marginBottom: '24px', padding: '24px' }" </a-form-item>
</a-form>
</a-card>
<GridLayout
v-model:layout="chartOrder"
:col-num="12"
:row-height="100"
:margin="[10, 10]"
:is-draggable="true"
:is-resizable="true"
:vertical-compact="true"
:use-css-transforms="true"
:responsive="true"
:prevent-collision="false"
@layout-updated="handleLayoutUpdated"
class="charts-container"
> >
<template #title>{{ type.toUpperCase() }}</template> <GridItem
v-for="item in chartOrder.filter(i => networkElementTypes.includes(i.i))"
:key="item.i"
:x="item.x"
:y="item.y"
:w="item.w"
:h="item.h"
:i="item.i"
:min-w="4"
:min-h="3"
:is-draggable="true"
:is-resizable="true"
:resizable-handles="['ne']"
drag-allow-from=".drag-handle"
drag-ignore-from=".no-drag"
class="grid-item"
>
<div class="drag-handle">
<DragOutlined />
</div>
<a-card :bordered="false" class="card-container">
<template #title>
<span class="no-drag">{{ item.i.toUpperCase() }}</span>
</template>
<template #extra> <template #extra>
<a-range-picker <a-range-picker
v-model:value="(rangePicker[type as keyof RangePicker]as[string,string])" v-model:value="rangePicker[item.i]"
:allow-clear="false" :allow-clear="false"
bordered bordered
:show-time="{ format: 'HH:mm:ss' }" :show-time="{ format: 'HH:mm:ss' }"
@@ -482,79 +695,106 @@ onUnmounted(() => {
:placeholder="rangePicker.placeholder" :placeholder="rangePicker.placeholder"
:ranges="rangePicker.ranges" :ranges="rangePicker.ranges"
style="width: 100%" style="width: 100%"
@change="() => fetchData(type)" @change="() => fetchData(item.i)"
></a-range-picker> ></a-range-picker>
</template> </template>
<div class="chart" style="padding: 12px"> <div class='chart'>
<div <div :ref="el => { if (el && chartStates[item.i]) chartStates[item.i].chartDom.value = el as HTMLElement }"></div>
:ref="el => { if (el) chartStates[type as ChartType].chartDom.value = el as HTMLElement }"
style="height: 400px; width: 100%"
></div>
</div> </div>
</a-card> </a-card>
</div> </GridItem>
</template> </GridLayout>
</draggable>
</PageContainer> </PageContainer>
</template> </template>
<style lang="less" scoped> <style lang="less" scoped>
.chart { .control-card {
width: 100%;
height: 450px;
}
.sortable-ghost {
opacity: 0.5;
background: #c8ebfb;
}
.sortable-drag {
opacity: 0.8;
background: #f4f4f4;
}
.row {
display: flex;
flex-wrap: wrap;
margin-right: -12px;
margin-left: -12px;
}
.col-lg-6 {
flex: 0 0 50%;
max-width: 50%;
padding-right: 12px;
padding-left: 12px;
}
@media (max-width: 991px) {
.col-md-6 {
flex: 0 0 50%;
max-width: 50%;
}
}
@media (max-width: 575px) {
.col-xs-12 {
flex: 0 0 100%;
max-width: 100%;
}
}
.control-row {
display: flex;
justify-content: flex-start;
align-items: center;
padding: 16px;
margin-bottom: 16px; margin-bottom: 16px;
background-color: #f0f2f5;
border-radius: 4px;
} }
.real-time-switch { .charts-container {
width: 100%;
min-height: 600px;
}
.grid-item {
overflow: visible;
position: relative;
}
.card-container {
height: 100%;
display: flex; display: flex;
align-items: center; flex-direction: column;
}
.chart {
flex: 1;
min-height: 0;
> div {
width: 100%;
height: 100%;
}
} }
.switch-label { .switch-label {
margin-left: 8px; margin-left: 8px;
font-size: 14px; font-size: 14px;
color: #333; color: rgba(0, 0, 0, 0.65);
white-space: nowrap; white-space: nowrap;
} }
.drag-handle {
position: absolute;
top: 0;
right: 0;
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
background-color: rgba(24, 144, 255, 0.1);
border-radius: 4px;
cursor: move;
z-index: 100;
transition: background-color 0.3s;
&:hover {
background-color: rgba(24, 144, 255, 0.2);
}
}
.ne-type-select {
flex-grow: 1;
max-width: 100%;
}
:deep {
.ant-card-body {
flex: 1;
display: flex;
flex-direction: column;
padding: 16px !important;
overflow: hidden;
}
.ant-form-item {
margin-bottom: 0;
}
.ant-form-item-label {
font-weight: 500;
color: rgba(0, 0, 0, 0.85);
}
.anticon {
font-size: 16px;
color: #1890ff;
}
.ant-select {
min-width: 200px;
}
}
</style> </style>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -18,6 +18,7 @@ import {
packetStart, packetStart,
packetStop, packetStop,
packetFilter, packetFilter,
packetKeep,
} from '@/api/trace/packet'; } from '@/api/trace/packet';
const ws = new WS(); const ws = new WS();
const { t } = useI18n(); const { t } = useI18n();
@@ -29,6 +30,8 @@ type StateType = {
devices: { id: string; label: string; children: any[] }[]; devices: { id: string; label: string; children: any[] }[];
/**初始化 */ /**初始化 */
initialized: boolean; initialized: boolean;
/**保活调度器 */
keepTimer: any;
/**任务 */ /**任务 */
task: { task: {
taskNo: string; taskNo: string;
@@ -64,6 +67,7 @@ type StateType = {
const state = reactive<StateType>({ const state = reactive<StateType>({
devices: [], devices: [],
initialized: false, initialized: false,
keepTimer: null,
task: { task: {
taskNo: 'laYlTbq', taskNo: 'laYlTbq',
device: '192.168.5.58', device: '192.168.5.58',
@@ -274,6 +278,9 @@ function wsMessage(res: Record<string, any>) {
// 建联时发送请求 // 建联时发送请求
if (!requestId && data.clientId) { if (!requestId && data.clientId) {
state.initialized = true; state.initialized = true;
state.keepTimer = setInterval(() => {
packetKeep(state.task.taskNo, 120);
}, 90 * 1000);
return; return;
} }
@@ -320,7 +327,9 @@ onMounted(() => {
}); });
onBeforeUnmount(() => { onBeforeUnmount(() => {
ws.close(); clearInterval(state.keepTimer);
state.keepTimer = null;
if (ws.state() === WebSocket.OPEN) ws.close();
}); });
</script> </script>