feat:添加WebSocket测试页面控制器
- 新增交互式WebSocket测试页面 - 提供完整的连接测试和消息发送功能 - 支持登录认证和聊天消息测试 - 包含位置更新和地图切换功能 - 提供实时消息日志和连接状态监控
This commit is contained in:
451
src/business/zulip/websocket_test.controller.ts
Normal file
451
src/business/zulip/websocket_test.controller.ts
Normal file
@@ -0,0 +1,451 @@
|
|||||||
|
/**
|
||||||
|
* WebSocket 测试页面控制器
|
||||||
|
*
|
||||||
|
* 提供一个简单的WebSocket测试界面,可以直接在浏览器中测试WebSocket连接
|
||||||
|
*
|
||||||
|
* @author moyin
|
||||||
|
* @version 1.0.0
|
||||||
|
* @since 2026-01-09
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Controller, Get, Res } from '@nestjs/common';
|
||||||
|
import { ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger';
|
||||||
|
import { Response } from 'express';
|
||||||
|
|
||||||
|
@ApiTags('websocket')
|
||||||
|
@Controller('websocket-test')
|
||||||
|
export class WebSocketTestController {
|
||||||
|
|
||||||
|
@Get()
|
||||||
|
@ApiOperation({
|
||||||
|
summary: 'WebSocket 测试页面',
|
||||||
|
description: '提供一个简单的WebSocket测试界面,可以直接在浏览器中测试连接和消息发送'
|
||||||
|
})
|
||||||
|
@ApiResponse({
|
||||||
|
status: 200,
|
||||||
|
description: 'WebSocket测试页面HTML',
|
||||||
|
content: {
|
||||||
|
'text/html': {
|
||||||
|
schema: {
|
||||||
|
type: 'string'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
getTestPage(@Res() res: Response) {
|
||||||
|
const html = `
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>WebSocket 测试工具 - Pixel Game Server</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 20px;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
background: white;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 20px;
|
||||||
|
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
.status {
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.status.disconnected { background-color: #ffebee; color: #c62828; }
|
||||||
|
.status.connected { background-color: #e8f5e8; color: #2e7d32; }
|
||||||
|
.status.connecting { background-color: #fff3e0; color: #ef6c00; }
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
input, textarea, select, button {
|
||||||
|
width: 100%;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
background-color: #1976d2;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
button:hover { background-color: #1565c0; }
|
||||||
|
button:disabled {
|
||||||
|
background-color: #ccc;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-log {
|
||||||
|
height: 300px;
|
||||||
|
overflow-y: auto;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
padding: 10px;
|
||||||
|
background-color: #fafafa;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
.message-item {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
.message-sent { background-color: #e3f2fd; }
|
||||||
|
.message-received { background-color: #f3e5f5; }
|
||||||
|
.message-system { background-color: #fff3e0; }
|
||||||
|
.message-error { background-color: #ffebee; }
|
||||||
|
|
||||||
|
.quick-actions {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||||
|
gap: 10px;
|
||||||
|
margin-top: 15px;
|
||||||
|
}
|
||||||
|
.quick-action {
|
||||||
|
padding: 8px 12px;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
.quick-action:hover { background-color: #eeeeee; }
|
||||||
|
|
||||||
|
.info-panel {
|
||||||
|
background-color: #e8f4fd;
|
||||||
|
border: 1px solid #bbdefb;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 15px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
.info-panel h3 {
|
||||||
|
margin-top: 0;
|
||||||
|
color: #1976d2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.two-column {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.two-column {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>🎮 Pixel Game Server - WebSocket 测试工具</h1>
|
||||||
|
|
||||||
|
<div class="info-panel">
|
||||||
|
<h3>📋 使用说明</h3>
|
||||||
|
<p><strong>1. 获取JWT Token:</strong> 先通过 <code>/auth/login</code> 接口获取有效的JWT Token</p>
|
||||||
|
<p><strong>2. 建立连接:</strong> 点击"连接"按钮建立WebSocket连接</p>
|
||||||
|
<p><strong>3. 用户登录:</strong> 输入JWT Token并点击"登录"进行认证</p>
|
||||||
|
<p><strong>4. 发送消息:</strong> 认证成功后可以发送聊天消息和位置更新</p>
|
||||||
|
<p><strong>WebSocket地址:</strong> <code>wss://whaletownend.xinghangee.icu/game</code></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="two-column">
|
||||||
|
<div class="container">
|
||||||
|
<h2>🔌 连接控制</h2>
|
||||||
|
<div id="connectionStatus" class="status disconnected">未连接</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="wsUrl">WebSocket 地址:</label>
|
||||||
|
<input type="text" id="wsUrl" value="wss://whaletownend.xinghangee.icu/game" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button id="connectBtn" onclick="toggleConnection()">连接</button>
|
||||||
|
|
||||||
|
<h3>🔐 用户认证</h3>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="jwtToken">JWT Token:</label>
|
||||||
|
<textarea id="jwtToken" rows="3" placeholder="请输入从 /auth/login 获取的JWT Token"></textarea>
|
||||||
|
</div>
|
||||||
|
<button id="loginBtn" onclick="login()" disabled>登录</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<h2>💬 消息发送</h2>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="messageType">消息类型:</label>
|
||||||
|
<select id="messageType">
|
||||||
|
<option value="chat">聊天消息</option>
|
||||||
|
<option value="position">位置更新</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="chatFields">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="chatContent">消息内容:</label>
|
||||||
|
<input type="text" id="chatContent" placeholder="输入聊天消息" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="chatScope">消息范围:</label>
|
||||||
|
<select id="chatScope">
|
||||||
|
<option value="local">本地 (当前地图)</option>
|
||||||
|
<option value="global">全局 (所有玩家)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="positionFields" style="display: none;">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="posX">X坐标:</label>
|
||||||
|
<input type="number" id="posX" value="150" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="posY">Y坐标:</label>
|
||||||
|
<input type="number" id="posY" value="400" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="mapId">地图ID:</label>
|
||||||
|
<select id="mapId">
|
||||||
|
<option value="whale_port">Whale Port (鲸鱼港)</option>
|
||||||
|
<option value="pumpkin_valley">Pumpkin Valley (南瓜谷)</option>
|
||||||
|
<option value="novice_village">Novice Village (新手村)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button id="sendBtn" onclick="sendMessage()" disabled>发送消息</button>
|
||||||
|
|
||||||
|
<div class="quick-actions">
|
||||||
|
<div class="quick-action" onclick="sendQuickMessage('Hello!')">快速发送: Hello!</div>
|
||||||
|
<div class="quick-action" onclick="sendQuickMessage('大家好!')">快速发送: 大家好!</div>
|
||||||
|
<div class="quick-action" onclick="sendQuickPosition()">发送位置更新</div>
|
||||||
|
<div class="quick-action" onclick="clearLog()">清空日志</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<h2>📋 消息日志</h2>
|
||||||
|
<div id="messageLog" class="message-log"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
let ws = null;
|
||||||
|
let isLoggedIn = false;
|
||||||
|
|
||||||
|
// 切换消息类型时显示对应字段
|
||||||
|
document.getElementById('messageType').addEventListener('change', function() {
|
||||||
|
const chatFields = document.getElementById('chatFields');
|
||||||
|
const positionFields = document.getElementById('positionFields');
|
||||||
|
|
||||||
|
if (this.value === 'chat') {
|
||||||
|
chatFields.style.display = 'block';
|
||||||
|
positionFields.style.display = 'none';
|
||||||
|
} else {
|
||||||
|
chatFields.style.display = 'none';
|
||||||
|
positionFields.style.display = 'block';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function updateStatus(status, message) {
|
||||||
|
const statusEl = document.getElementById('connectionStatus');
|
||||||
|
statusEl.className = 'status ' + status;
|
||||||
|
statusEl.textContent = message;
|
||||||
|
}
|
||||||
|
|
||||||
|
function addMessage(type, content, timestamp = new Date()) {
|
||||||
|
const log = document.getElementById('messageLog');
|
||||||
|
const messageEl = document.createElement('div');
|
||||||
|
messageEl.className = 'message-item message-' + type;
|
||||||
|
|
||||||
|
const timeStr = timestamp.toLocaleTimeString();
|
||||||
|
messageEl.innerHTML = '<strong>[' + timeStr + ']</strong> ' + content;
|
||||||
|
|
||||||
|
log.appendChild(messageEl);
|
||||||
|
log.scrollTop = log.scrollHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleConnection() {
|
||||||
|
if (ws && ws.readyState === WebSocket.OPEN) {
|
||||||
|
ws.close();
|
||||||
|
} else {
|
||||||
|
connect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function connect() {
|
||||||
|
const url = document.getElementById('wsUrl').value;
|
||||||
|
updateStatus('connecting', '连接中...');
|
||||||
|
|
||||||
|
try {
|
||||||
|
ws = new WebSocket(url);
|
||||||
|
|
||||||
|
ws.onopen = function() {
|
||||||
|
updateStatus('connected', '已连接');
|
||||||
|
document.getElementById('connectBtn').textContent = '断开';
|
||||||
|
document.getElementById('loginBtn').disabled = false;
|
||||||
|
addMessage('system', '✅ WebSocket连接成功');
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.onmessage = function(event) {
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(event.data);
|
||||||
|
addMessage('received', '📥 ' + JSON.stringify(data, null, 2));
|
||||||
|
|
||||||
|
// 处理特定消息类型
|
||||||
|
if (data.t === 'login_success') {
|
||||||
|
isLoggedIn = true;
|
||||||
|
document.getElementById('sendBtn').disabled = false;
|
||||||
|
addMessage('system', '✅ 登录成功!用户: ' + data.username);
|
||||||
|
} else if (data.t === 'login_error') {
|
||||||
|
addMessage('error', '❌ 登录失败: ' + data.message);
|
||||||
|
} else if (data.t === 'chat_render') {
|
||||||
|
addMessage('system', '💬 收到消息: ' + data.from + ' 说: ' + data.txt);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
addMessage('received', '📥 ' + event.data);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.onclose = function() {
|
||||||
|
updateStatus('disconnected', '未连接');
|
||||||
|
document.getElementById('connectBtn').textContent = '连接';
|
||||||
|
document.getElementById('loginBtn').disabled = true;
|
||||||
|
document.getElementById('sendBtn').disabled = true;
|
||||||
|
isLoggedIn = false;
|
||||||
|
addMessage('system', '🔌 WebSocket连接已关闭');
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.onerror = function(error) {
|
||||||
|
addMessage('error', '❌ 连接错误: ' + error);
|
||||||
|
};
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
updateStatus('disconnected', '连接失败');
|
||||||
|
addMessage('error', '❌ 连接失败: ' + error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function login() {
|
||||||
|
if (!ws || ws.readyState !== WebSocket.OPEN) {
|
||||||
|
addMessage('error', '❌ 请先建立WebSocket连接');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const token = document.getElementById('jwtToken').value.trim();
|
||||||
|
if (!token) {
|
||||||
|
addMessage('error', '❌ 请输入JWT Token');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const message = {
|
||||||
|
type: 'login',
|
||||||
|
token: token
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.send(JSON.stringify(message));
|
||||||
|
addMessage('sent', '📤 ' + JSON.stringify(message, null, 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendMessage() {
|
||||||
|
if (!ws || ws.readyState !== WebSocket.OPEN) {
|
||||||
|
addMessage('error', '❌ 请先建立WebSocket连接');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isLoggedIn) {
|
||||||
|
addMessage('error', '❌ 请先登录');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const messageType = document.getElementById('messageType').value;
|
||||||
|
let message;
|
||||||
|
|
||||||
|
if (messageType === 'chat') {
|
||||||
|
const content = document.getElementById('chatContent').value.trim();
|
||||||
|
if (!content) {
|
||||||
|
addMessage('error', '❌ 请输入消息内容');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
message = {
|
||||||
|
t: 'chat',
|
||||||
|
content: content,
|
||||||
|
scope: document.getElementById('chatScope').value
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
message = {
|
||||||
|
t: 'position',
|
||||||
|
x: parseInt(document.getElementById('posX').value),
|
||||||
|
y: parseInt(document.getElementById('posY').value),
|
||||||
|
mapId: document.getElementById('mapId').value
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
ws.send(JSON.stringify(message));
|
||||||
|
addMessage('sent', '📤 ' + JSON.stringify(message, null, 2));
|
||||||
|
|
||||||
|
// 清空聊天输入框
|
||||||
|
if (messageType === 'chat') {
|
||||||
|
document.getElementById('chatContent').value = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendQuickMessage(content) {
|
||||||
|
if (!isLoggedIn) {
|
||||||
|
addMessage('error', '❌ 请先登录');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('messageType').value = 'chat';
|
||||||
|
document.getElementById('chatContent').value = content;
|
||||||
|
document.getElementById('messageType').dispatchEvent(new Event('change'));
|
||||||
|
sendMessage();
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendQuickPosition() {
|
||||||
|
if (!isLoggedIn) {
|
||||||
|
addMessage('error', '❌ 请先登录');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('messageType').value = 'position';
|
||||||
|
document.getElementById('messageType').dispatchEvent(new Event('change'));
|
||||||
|
sendMessage();
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearLog() {
|
||||||
|
document.getElementById('messageLog').innerHTML = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 页面加载完成后的初始化
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
addMessage('system', '🎮 WebSocket测试工具已就绪');
|
||||||
|
addMessage('system', '💡 提示: 请先通过 /auth/login 接口获取JWT Token');
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`;
|
||||||
|
|
||||||
|
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
||||||
|
res.send(html);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user