534 lines
12 KiB
Vue
534 lines
12 KiB
Vue
|
|
<template>
|
|||
|
|
<view class="test-page">
|
|||
|
|
<view class="test-header">
|
|||
|
|
<text class="title">WebSocket 测试工具</text>
|
|||
|
|
<text class="subtitle">实时测试 WebSocket 连接和消息</text>
|
|||
|
|
</view>
|
|||
|
|
|
|||
|
|
<!-- 连接状态 -->
|
|||
|
|
<view class="status-card">
|
|||
|
|
<view class="status-item">
|
|||
|
|
<text class="label">连接状态:</text>
|
|||
|
|
<text :class="['value', connectionStatus.class]">{{ connectionStatus.text }}</text>
|
|||
|
|
</view>
|
|||
|
|
<view class="status-item">
|
|||
|
|
<text class="label">队列消息:</text>
|
|||
|
|
<text class="value">{{ queueLength }} 条</text>
|
|||
|
|
</view>
|
|||
|
|
<view class="status-item">
|
|||
|
|
<text class="label">重连次数:</text>
|
|||
|
|
<text class="value">{{ reconnectAttempts }} 次</text>
|
|||
|
|
</view>
|
|||
|
|
</view>
|
|||
|
|
|
|||
|
|
<!-- 连接控制 -->
|
|||
|
|
<view class="control-card">
|
|||
|
|
<view class="control-title">连接控制</view>
|
|||
|
|
<input
|
|||
|
|
class="url-input"
|
|||
|
|
v-model="wsUrl"
|
|||
|
|
placeholder="输入 WebSocket 地址"
|
|||
|
|
/>
|
|||
|
|
<view class="button-group">
|
|||
|
|
<button class="btn btn-primary" @click="handleConnect" :disabled="isConnected">
|
|||
|
|
连接
|
|||
|
|
</button>
|
|||
|
|
<button class="btn btn-danger" @click="handleDisconnect" :disabled="!isConnected">
|
|||
|
|
断开
|
|||
|
|
</button>
|
|||
|
|
</view>
|
|||
|
|
</view>
|
|||
|
|
|
|||
|
|
<!-- 消息发送 -->
|
|||
|
|
<view class="message-card">
|
|||
|
|
<view class="control-title">发送消息</view>
|
|||
|
|
<textarea
|
|||
|
|
class="message-input"
|
|||
|
|
v-model="messageToSend"
|
|||
|
|
placeholder="输入消息内容(JSON 格式)"
|
|||
|
|
:rows="5"
|
|||
|
|
></textarea>
|
|||
|
|
<view class="button-group">
|
|||
|
|
<button class="btn btn-success" @click="handleSendMessage" :disabled="!isConnected">
|
|||
|
|
发送消息
|
|||
|
|
</button>
|
|||
|
|
<button class="btn btn-secondary" @click="handleSendPing" :disabled="!isConnected">
|
|||
|
|
发送心跳
|
|||
|
|
</button>
|
|||
|
|
</view>
|
|||
|
|
</view>
|
|||
|
|
|
|||
|
|
<!-- 快速测试 -->
|
|||
|
|
<view class="quick-test-card">
|
|||
|
|
<view class="control-title">快速测试</view>
|
|||
|
|
<view class="button-group">
|
|||
|
|
<button class="btn btn-info" @click="testEcho">
|
|||
|
|
Echo 服务测试
|
|||
|
|
</button>
|
|||
|
|
<button class="btn btn-info" @click="testLocalServer">
|
|||
|
|
本地服务器测试
|
|||
|
|
</button>
|
|||
|
|
</view>
|
|||
|
|
</view>
|
|||
|
|
|
|||
|
|
<!-- 日志显示 -->
|
|||
|
|
<view class="log-card">
|
|||
|
|
<view class="log-header">
|
|||
|
|
<text class="control-title">日志 ({{ logs.length }})</text>
|
|||
|
|
<button class="btn-clear" @click="clearLogs">清空</button>
|
|||
|
|
</view>
|
|||
|
|
<scroll-view class="log-content" scroll-y>
|
|||
|
|
<view
|
|||
|
|
v-for="(log, index) in logs"
|
|||
|
|
:key="index"
|
|||
|
|
:class="['log-item', `log-${log.type}`]"
|
|||
|
|
>
|
|||
|
|
<text class="log-time">{{ log.time }}</text>
|
|||
|
|
<text class="log-type">{{ log.typeText }}</text>
|
|||
|
|
<text class="log-message">{{ log.message }}</text>
|
|||
|
|
</view>
|
|||
|
|
<view v-if="logs.length === 0" class="empty-log">
|
|||
|
|
暂无日志
|
|||
|
|
</view>
|
|||
|
|
</scroll-view>
|
|||
|
|
</view>
|
|||
|
|
</view>
|
|||
|
|
</template>
|
|||
|
|
|
|||
|
|
<script>
|
|||
|
|
import wsManager from '@/utils/websocket-manager.js'
|
|||
|
|
|
|||
|
|
export default {
|
|||
|
|
name: 'WebSocketTest',
|
|||
|
|
data() {
|
|||
|
|
return {
|
|||
|
|
// WebSocket URL
|
|||
|
|
wsUrl: 'wss://echo.websocket.org',
|
|||
|
|
|
|||
|
|
// 连接状态
|
|||
|
|
isConnected: false,
|
|||
|
|
reconnectAttempts: 0,
|
|||
|
|
queueLength: 0,
|
|||
|
|
|
|||
|
|
// 消息
|
|||
|
|
messageToSend: JSON.stringify({
|
|||
|
|
type: 'test',
|
|||
|
|
content: 'Hello WebSocket!',
|
|||
|
|
timestamp: Date.now()
|
|||
|
|
}, null, 2),
|
|||
|
|
|
|||
|
|
// 日志
|
|||
|
|
logs: [],
|
|||
|
|
|
|||
|
|
// 计时器
|
|||
|
|
startTime: 0
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
computed: {
|
|||
|
|
connectionStatus() {
|
|||
|
|
if (this.isConnected) {
|
|||
|
|
return { text: '已连接', class: 'status-connected' }
|
|||
|
|
} else if (this.reconnectAttempts > 0) {
|
|||
|
|
return { text: '重连中...', class: 'status-reconnecting' }
|
|||
|
|
} else {
|
|||
|
|
return { text: '未连接', class: 'status-disconnected' }
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
onLoad() {
|
|||
|
|
this.setupEventListeners()
|
|||
|
|
this.addLog('info', '测试工具已加载')
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
onUnload() {
|
|||
|
|
// 清理事件监听
|
|||
|
|
wsManager.off('open', this.handleOpen)
|
|||
|
|
wsManager.off('close', this.handleClose)
|
|||
|
|
wsManager.off('error', this.handleError)
|
|||
|
|
wsManager.off('message', this.handleMessage)
|
|||
|
|
wsManager.off('reconnect', this.handleReconnect)
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
methods: {
|
|||
|
|
// 设置事件监听
|
|||
|
|
setupEventListeners() {
|
|||
|
|
this.handleOpen = () => {
|
|||
|
|
this.isConnected = true
|
|||
|
|
this.reconnectAttempts = 0
|
|||
|
|
const duration = Date.now() - this.startTime
|
|||
|
|
this.addLog('success', `连接成功 (耗时 ${duration}ms)`)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
this.handleClose = (res) => {
|
|||
|
|
this.isConnected = false
|
|||
|
|
this.addLog('warning', `连接关闭 (code: ${res.code})`)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
this.handleError = (err) => {
|
|||
|
|
this.addLog('error', `连接错误: ${JSON.stringify(err)}`)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
this.handleMessage = (data) => {
|
|||
|
|
this.addLog('message', `收到消息: ${JSON.stringify(data, null, 2)}`)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
this.handleReconnect = (attempts) => {
|
|||
|
|
this.reconnectAttempts = attempts
|
|||
|
|
this.addLog('info', `正在重连 (第 ${attempts} 次)`)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
wsManager.on('open', this.handleOpen)
|
|||
|
|
wsManager.on('close', this.handleClose)
|
|||
|
|
wsManager.on('error', this.handleError)
|
|||
|
|
wsManager.on('message', this.handleMessage)
|
|||
|
|
wsManager.on('reconnect', this.handleReconnect)
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
// 连接
|
|||
|
|
handleConnect() {
|
|||
|
|
if (!this.wsUrl) {
|
|||
|
|
uni.showToast({ title: '请输入 WebSocket 地址', icon: 'none' })
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
this.addLog('info', `开始连接: ${this.wsUrl}`)
|
|||
|
|
this.startTime = Date.now()
|
|||
|
|
|
|||
|
|
wsManager.connect(this.wsUrl, {
|
|||
|
|
reconnectInterval: 3000,
|
|||
|
|
maxReconnectAttempts: 5,
|
|||
|
|
heartbeatInterval: 30000
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
this.updateState()
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
// 断开连接
|
|||
|
|
handleDisconnect() {
|
|||
|
|
this.addLog('info', '手动断开连接')
|
|||
|
|
wsManager.close(true)
|
|||
|
|
this.isConnected = false
|
|||
|
|
this.reconnectAttempts = 0
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
// 发送消息
|
|||
|
|
handleSendMessage() {
|
|||
|
|
try {
|
|||
|
|
const message = JSON.parse(this.messageToSend)
|
|||
|
|
this.addLog('send', `发送消息: ${JSON.stringify(message)}`)
|
|||
|
|
wsManager.send(message)
|
|||
|
|
} catch (e) {
|
|||
|
|
this.addLog('error', `消息格式错误: ${e.message}`)
|
|||
|
|
uni.showToast({ title: 'JSON 格式错误', icon: 'none' })
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
// 发送心跳
|
|||
|
|
handleSendPing() {
|
|||
|
|
const ping = {
|
|||
|
|
type: 'ping',
|
|||
|
|
timestamp: Date.now()
|
|||
|
|
}
|
|||
|
|
this.addLog('send', `发送心跳: ${JSON.stringify(ping)}`)
|
|||
|
|
wsManager.send(ping)
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
// Echo 服务测试
|
|||
|
|
testEcho() {
|
|||
|
|
this.wsUrl = 'wss://echo.websocket.org'
|
|||
|
|
this.messageToSend = JSON.stringify({
|
|||
|
|
type: 'test',
|
|||
|
|
content: 'Echo 测试消息',
|
|||
|
|
timestamp: Date.now()
|
|||
|
|
}, null, 2)
|
|||
|
|
|
|||
|
|
this.addLog('info', '开始 Echo 服务测试')
|
|||
|
|
uni.showToast({ title: '点击"连接"开始测试', icon: 'none' })
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
// 本地服务器测试
|
|||
|
|
testLocalServer() {
|
|||
|
|
this.wsUrl = 'ws://localhost:8082/ws/chat?token=test-token'
|
|||
|
|
this.messageToSend = JSON.stringify({
|
|||
|
|
type: 'message',
|
|||
|
|
fromUserId: 'test_user',
|
|||
|
|
toUserId: 'admin',
|
|||
|
|
content: '本地服务器测试',
|
|||
|
|
timestamp: Date.now()
|
|||
|
|
}, null, 2)
|
|||
|
|
|
|||
|
|
this.addLog('info', '开始本地服务器测试')
|
|||
|
|
uni.showToast({ title: '点击"连接"开始测试', icon: 'none' })
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
// 更新状态
|
|||
|
|
updateState() {
|
|||
|
|
const state = wsManager.getState()
|
|||
|
|
this.isConnected = state.isConnected
|
|||
|
|
this.reconnectAttempts = state.reconnectAttempts
|
|||
|
|
this.queueLength = state.queueLength
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
// 添加日志
|
|||
|
|
addLog(type, message) {
|
|||
|
|
const typeMap = {
|
|||
|
|
info: 'ℹ️ 信息',
|
|||
|
|
success: '✅ 成功',
|
|||
|
|
warning: '⚠️ 警告',
|
|||
|
|
error: '❌ 错误',
|
|||
|
|
send: '📤 发送',
|
|||
|
|
message: '📩 接收'
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const now = new Date()
|
|||
|
|
const time = `${now.getHours().toString().padStart(2, '0')}:${now.getMinutes().toString().padStart(2, '0')}:${now.getSeconds().toString().padStart(2, '0')}`
|
|||
|
|
|
|||
|
|
this.logs.unshift({
|
|||
|
|
type,
|
|||
|
|
typeText: typeMap[type] || type,
|
|||
|
|
message,
|
|||
|
|
time
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
// 最多保留 100 条日志
|
|||
|
|
if (this.logs.length > 100) {
|
|||
|
|
this.logs.pop()
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
// 清空日志
|
|||
|
|
clearLogs() {
|
|||
|
|
this.logs = []
|
|||
|
|
this.addLog('info', '日志已清空')
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
</script>
|
|||
|
|
|
|||
|
|
<style scoped>
|
|||
|
|
.test-page {
|
|||
|
|
padding: 20rpx;
|
|||
|
|
background-color: #f5f6fa;
|
|||
|
|
min-height: 100vh;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.test-header {
|
|||
|
|
text-align: center;
|
|||
|
|
padding: 40rpx 0;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.title {
|
|||
|
|
font-size: 36rpx;
|
|||
|
|
font-weight: bold;
|
|||
|
|
color: #333;
|
|||
|
|
display: block;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.subtitle {
|
|||
|
|
font-size: 24rpx;
|
|||
|
|
color: #999;
|
|||
|
|
margin-top: 10rpx;
|
|||
|
|
display: block;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.status-card,
|
|||
|
|
.control-card,
|
|||
|
|
.message-card,
|
|||
|
|
.quick-test-card,
|
|||
|
|
.log-card {
|
|||
|
|
background: #fff;
|
|||
|
|
border-radius: 16rpx;
|
|||
|
|
padding: 30rpx;
|
|||
|
|
margin-bottom: 20rpx;
|
|||
|
|
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.status-item {
|
|||
|
|
display: flex;
|
|||
|
|
justify-content: space-between;
|
|||
|
|
align-items: center;
|
|||
|
|
padding: 20rpx 0;
|
|||
|
|
border-bottom: 1rpx solid #f0f0f0;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.status-item:last-child {
|
|||
|
|
border-bottom: none;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.label {
|
|||
|
|
font-size: 28rpx;
|
|||
|
|
color: #666;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.value {
|
|||
|
|
font-size: 28rpx;
|
|||
|
|
font-weight: 500;
|
|||
|
|
color: #333;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.status-connected {
|
|||
|
|
color: #52c41a;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.status-reconnecting {
|
|||
|
|
color: #faad14;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.status-disconnected {
|
|||
|
|
color: #999;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.control-title {
|
|||
|
|
font-size: 30rpx;
|
|||
|
|
font-weight: 500;
|
|||
|
|
color: #333;
|
|||
|
|
margin-bottom: 20rpx;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.url-input,
|
|||
|
|
.message-input {
|
|||
|
|
width: 100%;
|
|||
|
|
padding: 20rpx;
|
|||
|
|
border: 1rpx solid #d9d9d9;
|
|||
|
|
border-radius: 8rpx;
|
|||
|
|
font-size: 26rpx;
|
|||
|
|
margin-bottom: 20rpx;
|
|||
|
|
box-sizing: border-box;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.message-input {
|
|||
|
|
min-height: 200rpx;
|
|||
|
|
font-family: 'Courier New', monospace;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.button-group {
|
|||
|
|
display: flex;
|
|||
|
|
gap: 20rpx;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.btn {
|
|||
|
|
flex: 1;
|
|||
|
|
height: 80rpx;
|
|||
|
|
line-height: 80rpx;
|
|||
|
|
text-align: center;
|
|||
|
|
border-radius: 8rpx;
|
|||
|
|
font-size: 28rpx;
|
|||
|
|
border: none;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.btn-primary {
|
|||
|
|
background: #1890ff;
|
|||
|
|
color: #fff;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.btn-danger {
|
|||
|
|
background: #ff4d4f;
|
|||
|
|
color: #fff;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.btn-success {
|
|||
|
|
background: #52c41a;
|
|||
|
|
color: #fff;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.btn-secondary {
|
|||
|
|
background: #d9d9d9;
|
|||
|
|
color: #666;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.btn-info {
|
|||
|
|
background: #722ed1;
|
|||
|
|
color: #fff;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.btn:disabled {
|
|||
|
|
opacity: 0.5;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.log-header {
|
|||
|
|
display: flex;
|
|||
|
|
justify-content: space-between;
|
|||
|
|
align-items: center;
|
|||
|
|
margin-bottom: 20rpx;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.btn-clear {
|
|||
|
|
padding: 10rpx 20rpx;
|
|||
|
|
background: #f0f0f0;
|
|||
|
|
border: none;
|
|||
|
|
border-radius: 8rpx;
|
|||
|
|
font-size: 24rpx;
|
|||
|
|
color: #666;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.log-content {
|
|||
|
|
height: 600rpx;
|
|||
|
|
border: 1rpx solid #f0f0f0;
|
|||
|
|
border-radius: 8rpx;
|
|||
|
|
padding: 20rpx;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.log-item {
|
|||
|
|
padding: 20rpx;
|
|||
|
|
margin-bottom: 10rpx;
|
|||
|
|
border-radius: 8rpx;
|
|||
|
|
font-size: 24rpx;
|
|||
|
|
line-height: 1.6;
|
|||
|
|
word-break: break-all;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.log-info {
|
|||
|
|
background: #e6f7ff;
|
|||
|
|
border-left: 4rpx solid #1890ff;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.log-success {
|
|||
|
|
background: #f6ffed;
|
|||
|
|
border-left: 4rpx solid #52c41a;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.log-warning {
|
|||
|
|
background: #fffbe6;
|
|||
|
|
border-left: 4rpx solid #faad14;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.log-error {
|
|||
|
|
background: #fff2f0;
|
|||
|
|
border-left: 4rpx solid #ff4d4f;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.log-send {
|
|||
|
|
background: #f9f0ff;
|
|||
|
|
border-left: 4rpx solid #722ed1;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.log-message {
|
|||
|
|
background: #fff0f6;
|
|||
|
|
border-left: 4rpx solid #eb2f96;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.log-time {
|
|||
|
|
color: #999;
|
|||
|
|
margin-right: 20rpx;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.log-type {
|
|||
|
|
font-weight: 500;
|
|||
|
|
margin-right: 20rpx;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.log-message {
|
|||
|
|
color: #333;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.empty-log {
|
|||
|
|
text-align: center;
|
|||
|
|
padding: 100rpx 0;
|
|||
|
|
color: #999;
|
|||
|
|
font-size: 28rpx;
|
|||
|
|
}
|
|||
|
|
</style>
|
|||
|
|
|