551 lines
11 KiB
Vue
551 lines
11 KiB
Vue
<template>
|
||
<div class="tool-detail-page">
|
||
|
||
<!-- 注意 -->
|
||
<n-card size="small" title="注意">
|
||
<n-p>由于浏览器限制,通过 HTTPS 访问网站时只能连接带 SSL 的 WebSocket(WSS)。</n-p>
|
||
<n-p>若需要连接不带 SSL 的 WebSocket(WS),建议下载到本地后使用。</n-p>
|
||
</n-card>
|
||
|
||
<!-- 输入 -->
|
||
<n-card size="small" title="输入">
|
||
<n-form
|
||
class="form-no-feedback"
|
||
label-align="left"
|
||
label-placement="top"
|
||
label-width="auto"
|
||
>
|
||
|
||
<n-form-item label="连接地址:">
|
||
<n-input-group class="address-input">
|
||
<n-select
|
||
v-model:value="data.address.prefix"
|
||
:options="[
|
||
{ label: 'ws://', value: 'ws://' },
|
||
{ label: 'wss://', value: 'wss://' },
|
||
]"
|
||
/>
|
||
<n-input
|
||
v-model:value="data.address.suffix"
|
||
type="text"
|
||
></n-input>
|
||
</n-input-group>
|
||
</n-form-item>
|
||
|
||
<n-form-item label="发送内容(自动移除换行符):">
|
||
<n-input
|
||
v-model:value="data.inputs"
|
||
placeholder="在此处输入要发送的内容"
|
||
type="textarea"
|
||
:rows="2"
|
||
></n-input>
|
||
</n-form-item>
|
||
|
||
</n-form>
|
||
</n-card>
|
||
|
||
<!-- 操作 -->
|
||
<n-card size="small" title="操作">
|
||
<n-flex>
|
||
<n-button
|
||
type="success"
|
||
:disabled="data.ws !== null"
|
||
@click="wsConnect"
|
||
>连接</n-button>
|
||
<n-button
|
||
type="warning"
|
||
:disabled="data.ws === null"
|
||
@click="wsClose"
|
||
>断开</n-button>
|
||
<n-button
|
||
type="primary"
|
||
:disabled="data.ws === null"
|
||
@click="wsSend"
|
||
>发送</n-button>
|
||
<n-button
|
||
type="error"
|
||
@click="clearInputs"
|
||
>清空输入</n-button>
|
||
<n-button
|
||
type="error"
|
||
@click="clearMessages"
|
||
>清空消息</n-button>
|
||
</n-flex>
|
||
</n-card>
|
||
|
||
<!-- 日志 -->
|
||
<n-card size="small" title="日志">
|
||
<div
|
||
ref="logsContentRef"
|
||
class="logs-content"
|
||
:style="{ height: (data.logsHeight + 'px') }"
|
||
>
|
||
<div class="message-list">
|
||
<div
|
||
v-for="item in data.messages"
|
||
:key="item.id"
|
||
class="message-item"
|
||
>
|
||
<!-- 时间 -->
|
||
<n-tag
|
||
:type="item.type === 'send' ? 'primary' : 'success'"
|
||
size="small"
|
||
>{{ getCommonDateTime(item.time) }}</n-tag>
|
||
<!-- 消息内容 -->
|
||
<div
|
||
v-if="data.parseType === 'html'"
|
||
v-html="item.message"
|
||
class="message-content"
|
||
></div>
|
||
<div
|
||
v-else
|
||
class="message-content"
|
||
>{{ item.message }}</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</n-card>
|
||
|
||
<!-- 设置 -->
|
||
<n-card size="small" title="设置">
|
||
<n-form
|
||
class="form-no-feedback"
|
||
label-align="left"
|
||
label-placement="top"
|
||
label-width="auto"
|
||
>
|
||
|
||
<!-- 日志高度 -->
|
||
<n-form-item label="日志高度">
|
||
<n-input-number
|
||
v-model:value="data.logsHeight"
|
||
:min="80"
|
||
:max="800"
|
||
:precision="0"
|
||
:step="1"
|
||
></n-input-number>
|
||
</n-form-item>
|
||
|
||
<!-- 日志最大行数 -->
|
||
<n-form-item label="日志最大行数">
|
||
<n-input-number
|
||
v-model:value="data.logsMax"
|
||
:min="1"
|
||
:max="8192"
|
||
:precision="0"
|
||
:step="1"
|
||
></n-input-number>
|
||
</n-form-item>
|
||
|
||
<!-- 解析类型 -->
|
||
<n-form-item label="解析类型">
|
||
<n-radio-group v-model:value="data.parseType">
|
||
<n-radio-button
|
||
v-for="item in data.parseTypes"
|
||
:key="item.name"
|
||
:label="item.label"
|
||
:value="item.name"
|
||
/>
|
||
</n-radio-group>
|
||
</n-form-item>
|
||
|
||
<!-- 自动滚动 -->
|
||
<n-form-item label="自动滚动">
|
||
<n-radio-group v-model:value="data.autoScroll">
|
||
<n-radio-button label="开启" :value="true" />
|
||
<n-radio-button label="关闭" :value="false" />
|
||
</n-radio-group>
|
||
</n-form-item>
|
||
|
||
</n-form>
|
||
</n-card>
|
||
|
||
</div>
|
||
</template>
|
||
|
||
<script setup>
|
||
import {
|
||
NButton, NCard,
|
||
NFlex, NForm, NFormItem,
|
||
NInput, NInputGroup, NInputNumber,
|
||
NP, NRadioButton, NRadioGroup, NSelect, NTag,
|
||
} from 'naive-ui';
|
||
|
||
import {
|
||
reactive, ref, nextTick, onMounted, onBeforeUnmount,
|
||
} from 'vue';
|
||
|
||
import {
|
||
$dialog, $notification,
|
||
} from '@/assets/js/naive-ui';
|
||
|
||
import getCommonDateTime from '@frost-utils/javascript/common/getCommonDateTime';
|
||
|
||
/** 数据 */
|
||
const data = reactive({
|
||
|
||
/** 连接地址 */
|
||
address: {
|
||
prefix: 'ws://',
|
||
suffix: '',
|
||
},
|
||
|
||
/** 自动滚动结果 */
|
||
autoScroll: true,
|
||
|
||
/** 发送内容 */
|
||
inputs: '',
|
||
|
||
/** 日志高度 */
|
||
logsHeight: 320,
|
||
|
||
/** 日志最大行数 */
|
||
logsMax: 100,
|
||
|
||
/** 接收内容 */
|
||
messages: [],
|
||
|
||
/** 消息 ID */
|
||
messageID: 0,
|
||
|
||
/** 消息解析类型 */
|
||
parseType: 'string',
|
||
|
||
/** 消息解析类型列表 */
|
||
parseTypes: [
|
||
{ name: 'html', label: 'HTML' },
|
||
{ name: 'json', label: 'JSON' },
|
||
{ name: 'string', label: '字符串' },
|
||
],
|
||
|
||
/**
|
||
* @desc WebSocket 对象
|
||
* @type {WebSocket}
|
||
*/
|
||
ws: null,
|
||
|
||
});
|
||
|
||
/**
|
||
* @desc 日志内容 ref
|
||
* @type {VueRef<HTMLElement>}
|
||
*/
|
||
const logsContentRef = ref(null);
|
||
|
||
/** 清空输入 */
|
||
function clearInputs() {
|
||
$dialog.create({
|
||
content: '确定要清空输入的内容吗?',
|
||
negativeText: '取消',
|
||
positiveText: '确定',
|
||
title: '确认',
|
||
type: 'default',
|
||
onPositiveClick: () => {
|
||
data.inputs = '';
|
||
},
|
||
});
|
||
}
|
||
|
||
/** 清空消息 */
|
||
function clearMessages() {
|
||
$dialog.create({
|
||
content: '确定要清空消息内容吗?',
|
||
negativeText: '取消',
|
||
positiveText: '确定',
|
||
title: '确认',
|
||
type: 'default',
|
||
onPositiveClick: () => {
|
||
data.messages = [];
|
||
data.messageID = 0;
|
||
},
|
||
});
|
||
}
|
||
|
||
/**
|
||
* @description 处理 WebSocket 关闭
|
||
* @param {CloseEvent} event
|
||
*/
|
||
function handleClose(event) {
|
||
|
||
let ws = event.target;
|
||
|
||
notify({
|
||
message: 'WebSocket 已关闭',
|
||
type: 'warning',
|
||
});
|
||
|
||
if (ws) {
|
||
ws.removeEventListener('close', handleClose);
|
||
ws.removeEventListener('error', handleError);
|
||
ws.removeEventListener('message', handleMessage);
|
||
ws.removeEventListener('open', handleOpen);
|
||
}
|
||
|
||
}
|
||
|
||
/** 处理 WebSocket 错误 */
|
||
function handleError() {
|
||
notify({
|
||
message: 'WebSocket 发生错误',
|
||
type: 'error',
|
||
});
|
||
}
|
||
|
||
/**
|
||
* @description 处理 WebSocket 消息
|
||
* @param {MessageEvent} ev
|
||
*/
|
||
function handleMessage(ev) {
|
||
|
||
let msg = ev.data;
|
||
let el = logsContentRef.value;
|
||
|
||
if (!msg) {
|
||
return;
|
||
}
|
||
|
||
let parsed = wsParse(false, msg);
|
||
let result = (parsed || msg)
|
||
|
||
console.log('%c%s', 'color: #2196F3;', '[接收]', (parsed || result));
|
||
|
||
// 记录消息
|
||
pushMessage('receive', msg);
|
||
|
||
// 自动滚动
|
||
nextTick(() => {
|
||
if (el && data.autoScroll) {
|
||
el.scrollTo(0, el.scrollHeight)
|
||
}
|
||
});
|
||
|
||
}
|
||
|
||
/** 处理 WebSocket 打开 */
|
||
function handleOpen() {
|
||
notify({
|
||
message: 'WebSocket 已连接',
|
||
type: 'info',
|
||
});
|
||
}
|
||
|
||
/** 初始化 */
|
||
function init() {
|
||
|
||
// 检测兼容性
|
||
if (typeof WebSocket === 'undefined') {
|
||
notify({
|
||
duration: 0,
|
||
message: '您的浏览器不支持 WebSocket。',
|
||
title: '错误',
|
||
type: 'error',
|
||
});
|
||
}
|
||
|
||
}
|
||
|
||
/**
|
||
* @description 提示信息
|
||
* @param {object} options
|
||
* @param {number} options.duration
|
||
* @param {string} options.message
|
||
* @param {string} options.title
|
||
* @param {string} options.type
|
||
*/
|
||
function notify(options) {
|
||
|
||
let {
|
||
duration = 3000,
|
||
message = '',
|
||
title = '提示',
|
||
type = 'info',
|
||
} = options;
|
||
|
||
return $notification.create({
|
||
content: message,
|
||
duration: duration,
|
||
title: title,
|
||
type: type,
|
||
});
|
||
|
||
}
|
||
|
||
/**
|
||
* @description 添加消息
|
||
* @param {string} type 类型(receive、send)
|
||
* @param {string} msg 消息内容
|
||
*/
|
||
function pushMessage(type, msg = '') {
|
||
|
||
let types = ['receive', 'send'];
|
||
|
||
if (types.indexOf(type) === -1) {
|
||
return;
|
||
}
|
||
|
||
let current = data.messages.length;
|
||
let max = data.logsMax;
|
||
|
||
// 最大行数
|
||
if (current >= max) {
|
||
data.messages.splice(0, (current - max + 1));
|
||
}
|
||
|
||
data.messageID += 1;
|
||
data.messages.push({
|
||
id: data.messageID,
|
||
message: msg,
|
||
time: Date.now(),
|
||
type: type,
|
||
});
|
||
|
||
}
|
||
|
||
/** 关闭连接 */
|
||
function wsClose() {
|
||
if (data.ws) {
|
||
data.ws.close();
|
||
data.ws = null;
|
||
}
|
||
}
|
||
|
||
/** 打开连接 */
|
||
function wsConnect() {
|
||
|
||
let info = data.address;
|
||
let address = (info.prefix + info.suffix);
|
||
|
||
if (!info.suffix) {
|
||
notify({
|
||
message: '请填写连接地址',
|
||
title: '连接失败',
|
||
type: 'warning',
|
||
});
|
||
return;
|
||
}
|
||
|
||
try {
|
||
|
||
let ws = new WebSocket(address);
|
||
|
||
// 监听事件
|
||
ws.addEventListener('close', handleClose);
|
||
ws.addEventListener('error', handleError);
|
||
ws.addEventListener('message', handleMessage);
|
||
ws.addEventListener('open', handleOpen);
|
||
|
||
// 保存对象
|
||
data.ws = ws;
|
||
|
||
} catch (error) {
|
||
notify({
|
||
message: String(error),
|
||
title: '连接失败',
|
||
type: 'error',
|
||
});
|
||
}
|
||
|
||
}
|
||
|
||
/**
|
||
* @description 解析消息
|
||
* @param {boolean} isSend 是否为发送,否则为接收
|
||
* @param {string} content 消息文本内容
|
||
* @returns {null|object|string} 成功则返回解析后的消息,否则返回 null
|
||
*/
|
||
function wsParse(isSend = false, content = '') {
|
||
|
||
let parseType = data.parseType;
|
||
|
||
if (parseType === 'json') {
|
||
|
||
// JSON
|
||
try {
|
||
return JSON.parse(content);
|
||
} catch (error) {
|
||
let s1 = isSend ? '解析发送的 JSON 消息失败' : '解析接收的 JSON 消息失败';
|
||
let s2 = String(error);
|
||
console.warn(s1);
|
||
console.warn(s2);
|
||
notify({
|
||
message: s2,
|
||
title: s1,
|
||
type: 'warning',
|
||
});
|
||
return null;
|
||
}
|
||
|
||
} else {
|
||
|
||
// 默认不处理
|
||
return content;
|
||
|
||
}
|
||
|
||
}
|
||
|
||
/** 发送消息 */
|
||
function wsSend() {
|
||
|
||
let instance = data.ws;
|
||
let message = data.inputs.replace(/(\n|\r)/g, '');
|
||
let parsed = wsParse(true, message);
|
||
|
||
if (instance && parsed) {
|
||
console.log('%c%s', 'color: #4CAF50;', '[发送]', parsed);
|
||
instance.send(message);
|
||
pushMessage('send', message);
|
||
}
|
||
|
||
}
|
||
|
||
onMounted(() => {
|
||
init();
|
||
});
|
||
|
||
onBeforeUnmount(() => {
|
||
wsClose();
|
||
});
|
||
</script>
|
||
|
||
<style lang="less" scoped>
|
||
.address-input {
|
||
.n-select {
|
||
width: 100px;
|
||
}
|
||
|
||
.n-input {
|
||
flex-grow: 1;
|
||
width: 0;
|
||
}
|
||
}
|
||
|
||
.logs-content {
|
||
padding: 8px 16px;
|
||
border: 1px solid #F0F0F0;
|
||
border-radius: 4px;
|
||
background-color: #FFF;
|
||
line-height: 1.6;
|
||
overflow-y: auto;
|
||
user-select: text;
|
||
|
||
.message-item {
|
||
display: flex;
|
||
margin: 0.5rem 0;
|
||
word-break: break-all;
|
||
|
||
> * {
|
||
margin: auto 0;
|
||
}
|
||
}
|
||
|
||
.n-tag {
|
||
margin-right: 0.5em;
|
||
}
|
||
|
||
.message-content {
|
||
position: relative;
|
||
}
|
||
}
|
||
</style>
|