550 lines
16 KiB
Vue
550 lines
16 KiB
Vue
<script setup lang="ts">
|
||
import { reactive, onMounted, onBeforeUnmount, ref } from 'vue';
|
||
import { PageContainer } from 'antdv-pro-layout';
|
||
import { message, Modal } from 'ant-design-vue/es';
|
||
import useI18n from '@/hooks/useI18n';
|
||
import {
|
||
RESULT_CODE_ERROR,
|
||
RESULT_CODE_SUCCESS,
|
||
} from '@/constants/result-constants';
|
||
import TerminalSSHView from '@/components/TerminalSSHView/index.vue';
|
||
import useNeInfoStore from '@/store/modules/neinfo';
|
||
import { iperfI, iperfV } from '@/api/tool/iperf';
|
||
const neInfoStore = useNeInfoStore();
|
||
const { t } = useI18n();
|
||
|
||
/**网元参数 */
|
||
let neCascaderOptions = ref<Record<string, any>[]>([]);
|
||
|
||
/**状态对象 */
|
||
let state = reactive({
|
||
/**初始化 */
|
||
initialized: false,
|
||
/**运行中 */
|
||
running: false,
|
||
/**版本信息 */
|
||
versionInfo: [],
|
||
/**网元类型 */
|
||
neType: [],
|
||
/**数据类型 */
|
||
dataType: 'options' as 'options' | 'command',
|
||
/**ws参数 */
|
||
params: {
|
||
neType: '',
|
||
neId: '',
|
||
cols: 120,
|
||
rows: 40,
|
||
},
|
||
/**ws数据 */
|
||
data: {
|
||
command: '', // 命令字符串
|
||
version: 'V3', // 服务版本,默认V3
|
||
mode: 'client', // 服务端或客户端,默认服务端
|
||
// Server or Client
|
||
port: 5201, // 服务端口
|
||
interval: 1, // 每次报告之间的时间间隔,单位为秒
|
||
// Server
|
||
oneOff: false, // 只进行一次连接
|
||
// Client
|
||
host: '', // 客户端连接到的服务端IP地址
|
||
udp: false, // use UDP rather than TCP
|
||
time: 10, // 以秒为单位的传输时间(默认为 10 秒)
|
||
reverse: false, // 以反向模式运行(服务器发送,客户端接收)
|
||
window: '300k', // 设置窗口大小/套接字缓冲区大小
|
||
parallel: 1, // 运行的并行客户端流数量
|
||
bitrate: 0, // 以比特/秒为单位(0 表示无限制)
|
||
},
|
||
});
|
||
|
||
/**连接发送 */
|
||
async function fnIPerf() {
|
||
const [neType, neId] = state.neType;
|
||
if (!neType || !neId) {
|
||
message.warning({
|
||
content: 'No Found NE Type',
|
||
duration: 2,
|
||
});
|
||
return;
|
||
}
|
||
if (state.dataType === 'options' && state.data.mode === 'client' && 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 (neType !== state.params.neType || neId !== state.params.neId) {
|
||
state.initialized = false;
|
||
state.params.neType = neType;
|
||
state.params.neId = neId;
|
||
}
|
||
|
||
// 软件版本检查
|
||
state.params.neType = neType;
|
||
state.params.neId = neId;
|
||
const resVersion = await iperfV({
|
||
neType,
|
||
neId,
|
||
version: state.data.version,
|
||
});
|
||
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;
|
||
}
|
||
|
||
// 初始化的直接重发
|
||
if (state.initialized) {
|
||
fnResend();
|
||
return;
|
||
}
|
||
state.initialized = true;
|
||
}
|
||
|
||
/**触发安装iperf */
|
||
function fnInstall() {
|
||
const key = 'iperfI';
|
||
message.loading({ content: t('common.loading'), key });
|
||
const { neType, neId } = state.params;
|
||
iperfI({ neType, neId, version: state.data.version }).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();
|
||
setTimeout(() => {
|
||
toolTerminal.value.clear();
|
||
const data = JSON.parse(JSON.stringify(state.data));
|
||
if (state.dataType === 'options') data.command = '';
|
||
toolTerminal.value.send('iperf', data);
|
||
}, 1000);
|
||
}
|
||
|
||
/**终端初始连接*/
|
||
function fnConnect() {
|
||
fnResend();
|
||
}
|
||
|
||
/**终端消息处理*/
|
||
function fnProcessMessage(data: string): string {
|
||
// 查找的开始输出标记
|
||
const parts: string[] = data.split('\u001b[?2004l\r');
|
||
if (parts.length > 0) {
|
||
if (parts[0].startsWith('^C') || parts[0].startsWith('\r')) {
|
||
return '';
|
||
}
|
||
let text = parts[parts.length - 1];
|
||
// 找到最后输出标记
|
||
let lestIndex = text.lastIndexOf('\u001b[?2004h\u001b]0;');
|
||
if (lestIndex !== -1) {
|
||
text = text.substring(0, lestIndex);
|
||
}
|
||
if (text === '' || text === '\r\n' || text.startsWith('^C')) {
|
||
return '';
|
||
}
|
||
// 是否还有最后输出标记
|
||
lestIndex = text.lastIndexOf('\u001b[?2004h');
|
||
if (lestIndex !== -1) {
|
||
text = text.substring(0, lestIndex);
|
||
}
|
||
if (text.endsWith('# ')) {
|
||
text = text.substring(0, text.lastIndexOf('\r\n') + 2);
|
||
}
|
||
|
||
// console.log({ parts, text });
|
||
if (parts.length > 1 && parts[0].startsWith('iperf')) {
|
||
return parts[0] + '\r\n' + text;
|
||
}
|
||
return text;
|
||
}
|
||
return data;
|
||
}
|
||
|
||
/**终端消息监听*/
|
||
function fnMessage(res: Record<string, any>) {
|
||
const { code, requestId, data } = res;
|
||
if (code === RESULT_CODE_ERROR) {
|
||
console.warn(res.msg);
|
||
return;
|
||
}
|
||
if (!requestId) return;
|
||
let lestIndex = data.lastIndexOf('unable to');
|
||
if (lestIndex !== -1) {
|
||
state.running = false;
|
||
return;
|
||
}
|
||
lestIndex = data.lastIndexOf('failed:');
|
||
if (lestIndex !== -1) {
|
||
state.running = false;
|
||
return;
|
||
}
|
||
lestIndex = data.lastIndexOf('iperf Done.');
|
||
if (lestIndex !== -1) {
|
||
state.running = false;
|
||
return;
|
||
}
|
||
}
|
||
|
||
/**钩子函数,界面打开初始化*/
|
||
onMounted(() => {
|
||
// 获取网元网元列表
|
||
neInfoStore.fnNelist().then(res => {
|
||
if (res.code === RESULT_CODE_SUCCESS) {
|
||
if (res.data.length > 0) {
|
||
// 过滤不可用的网元
|
||
for (const item of neInfoStore.getNeCascaderOptions) {
|
||
neCascaderOptions.value.push(item);
|
||
}
|
||
|
||
if (neCascaderOptions.value.length === 0) {
|
||
message.warning({
|
||
content: t('common.noData'),
|
||
duration: 2,
|
||
});
|
||
return;
|
||
}
|
||
}
|
||
} else {
|
||
message.warning({
|
||
content: t('common.noData'),
|
||
duration: 2,
|
||
});
|
||
}
|
||
});
|
||
});
|
||
|
||
/**钩子函数,界面关闭*/
|
||
onBeforeUnmount(() => {});
|
||
</script>
|
||
|
||
<template>
|
||
<PageContainer>
|
||
<a-card :bordered="false" :body-style="{ padding: '0px' }">
|
||
<!-- 插槽-卡片左侧侧 -->
|
||
<template #title>
|
||
<a-space :size="8">
|
||
<span>
|
||
{{ t('views.ne.common.neType') }}:
|
||
<a-cascader
|
||
v-model:value="state.neType"
|
||
:options="neCascaderOptions"
|
||
:placeholder="t('common.selectPlease')"
|
||
:disabled="state.running"
|
||
/>
|
||
</span>
|
||
<a-radio-group
|
||
v-model:value="state.dataType"
|
||
button-style="solid"
|
||
:disabled="state.running"
|
||
>
|
||
<a-radio-button value="options">Options</a-radio-button>
|
||
<a-radio-button value="command">Command</a-radio-button>
|
||
</a-radio-group>
|
||
</a-space>
|
||
</template>
|
||
|
||
<!-- 插槽-卡片右侧 -->
|
||
<template #extra>
|
||
<a-space :size="8">
|
||
<a-button
|
||
@click.prevent="fnIPerf()"
|
||
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-row :gutter="16">
|
||
<a-col :span="4" :offset="2">
|
||
<a-form-item label="Mode" name="mode">
|
||
<a-radio-group
|
||
v-model:value="state.data.mode"
|
||
:disabled="state.running"
|
||
button-style="solid"
|
||
size="small"
|
||
>
|
||
<a-radio-button value="client">Client</a-radio-button>
|
||
<a-radio-button value="server">Server</a-radio-button>
|
||
</a-radio-group>
|
||
</a-form-item>
|
||
</a-col>
|
||
<a-col :span="4">
|
||
<a-form-item
|
||
label="Version"
|
||
name="version"
|
||
:label-col="{ span: 8 }"
|
||
:label-wrap="true"
|
||
>
|
||
<a-radio-group
|
||
v-model:value="state.data.version"
|
||
:disabled="state.running"
|
||
button-style="solid"
|
||
size="small"
|
||
>
|
||
<a-radio-button value="V2">V2</a-radio-button>
|
||
<a-radio-button value="V3">V3</a-radio-button>
|
||
</a-radio-group>
|
||
</a-form-item>
|
||
</a-col>
|
||
<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>
|
||
|
||
<template v-if="state.data.mode === 'client'">
|
||
<a-form-item
|
||
label="Host"
|
||
name="host"
|
||
help="run in client mode, connecting to <host>"
|
||
:label-col="{ span: 3 }"
|
||
:wrapper-col="{ span: 9 }"
|
||
: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>
|
||
<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"
|
||
></a-input-number>
|
||
</a-form-item>
|
||
<a-form-item
|
||
label="Parallel"
|
||
name="parallel"
|
||
help="number of parallel client streams to run"
|
||
>
|
||
<a-input-number
|
||
v-model:value="state.data.parallel"
|
||
:disabled="state.running"
|
||
allow-clear
|
||
:placeholder="t('common.inputPlease')"
|
||
style="width: 100%"
|
||
:min="1"
|
||
></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-form-item
|
||
label="Bitrate"
|
||
name="bitrate"
|
||
help="target bitrate in bits/sec (0 for unlimited)"
|
||
>
|
||
<a-input-number
|
||
v-model:value="state.data.bitrate"
|
||
:disabled="state.running"
|
||
allow-clear
|
||
:placeholder="t('common.inputPlease')"
|
||
style="width: 100%"
|
||
:min="1"
|
||
></a-input-number>
|
||
</a-form-item>
|
||
</a-col>
|
||
</a-row>
|
||
</template>
|
||
<a-row 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-input-group compact>
|
||
<a-select
|
||
v-model:value="state.data.version"
|
||
:disabled="state.running"
|
||
>
|
||
<a-select-option value="V2">iperf</a-select-option>
|
||
<a-select-option value="V3">iperf3</a-select-option>
|
||
</a-select>
|
||
<a-auto-complete
|
||
v-model:value="state.data.command"
|
||
:disabled="state.running"
|
||
:options="[
|
||
{ value: '--help' },
|
||
{ value: '-c 172.5.16.100 -p 5201' },
|
||
{ value: '-s -p 5201' },
|
||
]"
|
||
style="width: 400px"
|
||
placeholder="client or server command"
|
||
/>
|
||
</a-input-group>
|
||
</div>
|
||
|
||
<!-- 运行过程 -->
|
||
<TerminalSSHView
|
||
v-if="state.initialized"
|
||
ref="toolTerminal"
|
||
:id="`V${Date.now()}`"
|
||
prefix="iperf"
|
||
url="/tool/iperf/run"
|
||
:ne-type="state.params.neType"
|
||
:ne-id="state.params.neId"
|
||
:rows="state.params.rows"
|
||
:cols="state.params.cols"
|
||
:process-messages="fnProcessMessage"
|
||
style="height: 400px"
|
||
@connect="fnConnect"
|
||
@message="fnMessage"
|
||
></TerminalSSHView>
|
||
</a-card>
|
||
</PageContainer>
|
||
</template>
|
||
|
||
<style lang="less" scoped></style>
|