|
|
@ -34,6 +34,13 @@ |
|
|
|
position: relative; |
|
|
|
z-index: 1; |
|
|
|
} |
|
|
|
.stats-card { |
|
|
|
background: rgba(255, 255, 255, 0.9); |
|
|
|
backdrop-filter: blur(5px); |
|
|
|
} |
|
|
|
.dark .stats-card { |
|
|
|
background: rgba(31, 41, 55, 0.9); |
|
|
|
} |
|
|
|
</style> |
|
|
|
</head> |
|
|
|
<body class="bg-gray-50 dark:bg-gray-900 h-screen flex flex-col"> |
|
|
@ -61,6 +68,14 @@ |
|
|
|
<div class="w-2 h-2 rounded-full bg-red-500 status-dot"></div> |
|
|
|
<span class="text-sm text-gray-600 dark:text-gray-300">未连接</span> |
|
|
|
</div> |
|
|
|
<div id="stats" class="flex items-center space-x-4 ml-4"> |
|
|
|
<span class="text-sm text-gray-600 dark:text-gray-300"> |
|
|
|
消息: <span id="messageCount">0</span> |
|
|
|
</span> |
|
|
|
<span class="text-sm text-gray-600 dark:text-gray-300"> |
|
|
|
连接: <span id="connectionCount">0</span> |
|
|
|
</span> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
<div class="flex space-x-2"> |
|
|
|
<button id="connectBtn" |
|
|
@ -83,11 +98,20 @@ |
|
|
|
<div class="max-w-4xl mx-auto h-full px-4 py-4"> |
|
|
|
<div class="flex justify-between items-center mb-2"> |
|
|
|
<h2 class="text-lg font-semibold text-gray-700 dark:text-gray-300">消息记录</h2> |
|
|
|
<button id="clearBtn" |
|
|
|
class="px-3 py-1 bg-gray-500 hover:bg-gray-600 text-white rounded-lg |
|
|
|
shadow-sm transition-colors duration-200"> |
|
|
|
清空消息 |
|
|
|
</button> |
|
|
|
<div class="flex space-x-2"> |
|
|
|
<select id="messageType" |
|
|
|
class="px-3 py-1 border border-gray-300 dark:border-gray-600 rounded-lg |
|
|
|
dark:bg-gray-700 dark:text-white"> |
|
|
|
<option value="message">普通消息</option> |
|
|
|
<option value="system">系统消息</option> |
|
|
|
<option value="error">错误消息</option> |
|
|
|
</select> |
|
|
|
<button id="clearBtn" |
|
|
|
class="px-3 py-1 bg-gray-500 hover:bg-gray-600 text-white rounded-lg |
|
|
|
shadow-sm transition-colors duration-200"> |
|
|
|
清空消息 |
|
|
|
</button> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
<div id="messages" class="h-full overflow-y-auto space-y-4"> |
|
|
|
<!-- 消息将在这里动态添加 --> |
|
|
@ -205,6 +229,12 @@ |
|
|
|
|
|
|
|
// WebSocket 相关代码 |
|
|
|
let socket; |
|
|
|
let reconnectAttempts = 0; |
|
|
|
const maxReconnectAttempts = 5; |
|
|
|
const reconnectDelay = 3000; |
|
|
|
let messageCount = 0; |
|
|
|
let connectionCount = 0; |
|
|
|
|
|
|
|
const statusElement = document.getElementById('status'); |
|
|
|
const statusDot = statusElement.querySelector('.status-dot'); |
|
|
|
const statusText = statusElement.querySelector('span:last-child'); |
|
|
@ -215,6 +245,9 @@ |
|
|
|
const sendBtn = document.getElementById('sendBtn'); |
|
|
|
const wsAddressInput = document.getElementById('wsAddress'); |
|
|
|
const clearBtn = document.getElementById('clearBtn'); |
|
|
|
const messageTypeSelect = document.getElementById('messageType'); |
|
|
|
const messageCountElement = document.getElementById('messageCount'); |
|
|
|
const connectionCountElement = document.getElementById('connectionCount'); |
|
|
|
|
|
|
|
// 更新状态显示 |
|
|
|
function updateStatus(connected) { |
|
|
@ -224,6 +257,8 @@ |
|
|
|
statusText.textContent = '已连接'; |
|
|
|
statusText.classList.remove('text-gray-600'); |
|
|
|
statusText.classList.add('text-green-600'); |
|
|
|
connectionCount++; |
|
|
|
connectionCountElement.textContent = connectionCount; |
|
|
|
} else { |
|
|
|
statusDot.classList.remove('bg-green-500'); |
|
|
|
statusDot.classList.add('bg-red-500'); |
|
|
@ -234,7 +269,7 @@ |
|
|
|
} |
|
|
|
|
|
|
|
// 添加消息到显示区域 |
|
|
|
function addMessage(message, isReceived = false) { |
|
|
|
function addMessage(message, isReceived = false, type = 'message') { |
|
|
|
const messageElement = document.createElement('div'); |
|
|
|
messageElement.className = `message flex ${isReceived ? 'justify-start' : 'justify-end'}`; |
|
|
|
|
|
|
@ -242,140 +277,145 @@ |
|
|
|
messageContent.className = `flex items-start space-x-2 max-w-[80%]`; |
|
|
|
|
|
|
|
const avatar = document.createElement('div'); |
|
|
|
avatar.className = `flex-shrink-0 w-8 h-8 rounded-full flex items-center justify-center |
|
|
|
text-white font-bold ${isReceived ? 'bg-gray-500' : 'bg-indigo-500'}`; |
|
|
|
avatar.textContent = isReceived ? 'S' : '我'; |
|
|
|
let avatarClass = 'flex-shrink-0 w-8 h-8 rounded-full flex items-center justify-center text-white font-bold'; |
|
|
|
|
|
|
|
switch(type) { |
|
|
|
case 'system': |
|
|
|
avatarClass += ' bg-blue-500'; |
|
|
|
avatar.textContent = '系'; |
|
|
|
break; |
|
|
|
case 'error': |
|
|
|
avatarClass += ' bg-red-500'; |
|
|
|
avatar.textContent = '错'; |
|
|
|
break; |
|
|
|
default: |
|
|
|
avatarClass += isReceived ? ' bg-gray-500' : ' bg-indigo-500'; |
|
|
|
avatar.textContent = isReceived ? 'S' : '我'; |
|
|
|
} |
|
|
|
|
|
|
|
avatar.className = avatarClass; |
|
|
|
|
|
|
|
const bubble = document.createElement('div'); |
|
|
|
bubble.className = `p-3 rounded-lg shadow-sm ${ |
|
|
|
isReceived |
|
|
|
? 'bg-white dark:bg-gray-700 text-gray-800 dark:text-gray-200' |
|
|
|
: 'bg-indigo-500 text-white' |
|
|
|
}`; |
|
|
|
bubble.textContent = message; |
|
|
|
let bubbleClass = 'p-3 rounded-lg shadow-sm'; |
|
|
|
|
|
|
|
if (isReceived) { |
|
|
|
messageContent.appendChild(avatar); |
|
|
|
messageContent.appendChild(bubble); |
|
|
|
} else { |
|
|
|
messageContent.appendChild(bubble); |
|
|
|
messageContent.appendChild(avatar); |
|
|
|
switch(type) { |
|
|
|
case 'system': |
|
|
|
bubbleClass += ' bg-blue-100 dark:bg-blue-900'; |
|
|
|
break; |
|
|
|
case 'error': |
|
|
|
bubbleClass += ' bg-red-100 dark:bg-red-900'; |
|
|
|
break; |
|
|
|
default: |
|
|
|
bubbleClass += isReceived ? ' bg-gray-100 dark:bg-gray-700' : ' bg-indigo-100 dark:bg-indigo-900'; |
|
|
|
} |
|
|
|
|
|
|
|
bubble.className = bubbleClass; |
|
|
|
bubble.textContent = message; |
|
|
|
|
|
|
|
messageContent.appendChild(avatar); |
|
|
|
messageContent.appendChild(bubble); |
|
|
|
messageElement.appendChild(messageContent); |
|
|
|
messagesContainer.appendChild(messageElement); |
|
|
|
messagesContainer.scrollTop = messagesContainer.scrollHeight; |
|
|
|
|
|
|
|
// 收到消息时更新3D场景 |
|
|
|
if (isReceived) { |
|
|
|
updateParticles(); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
// 更新粒子效果 |
|
|
|
function updateParticles() { |
|
|
|
const positions = particleGeometry.attributes.position.array; |
|
|
|
const colors = particleGeometry.attributes.color.array; |
|
|
|
|
|
|
|
// 随机更新一些粒子的位置和颜色 |
|
|
|
for (let i = 0; i < particleCount; i += 10) { |
|
|
|
positions[i * 3] = (Math.random() - 0.5) * 20; |
|
|
|
positions[i * 3 + 1] = (Math.random() - 0.5) * 20; |
|
|
|
positions[i * 3 + 2] = (Math.random() - 0.5) * 20; |
|
|
|
|
|
|
|
colors[i * 3] = Math.random(); |
|
|
|
colors[i * 3 + 1] = Math.random(); |
|
|
|
colors[i * 3 + 2] = Math.random(); |
|
|
|
} |
|
|
|
// 滚动到底部 |
|
|
|
messagesContainer.scrollTop = messagesContainer.scrollHeight; |
|
|
|
|
|
|
|
particleGeometry.attributes.position.needsUpdate = true; |
|
|
|
particleGeometry.attributes.color.needsUpdate = true; |
|
|
|
// 更新消息计数 |
|
|
|
messageCount++; |
|
|
|
messageCountElement.textContent = messageCount; |
|
|
|
} |
|
|
|
|
|
|
|
// 初始化 WebSocket 连接 |
|
|
|
function initWebSocket() { |
|
|
|
const wsAddress = wsAddressInput.value.trim(); |
|
|
|
if (!wsAddress) { |
|
|
|
addMessage('请输入WebSocket地址', true); |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
// 连接WebSocket |
|
|
|
function connect() { |
|
|
|
try { |
|
|
|
// 确保地址以 ws:// 开头 |
|
|
|
const normalizedAddress = wsAddress.startsWith('ws://') |
|
|
|
? wsAddress |
|
|
|
: `ws://${wsAddress}`; |
|
|
|
|
|
|
|
socket = new WebSocket(normalizedAddress); |
|
|
|
|
|
|
|
socket = new WebSocket(wsAddressInput.value); |
|
|
|
|
|
|
|
socket.onopen = function() { |
|
|
|
updateStatus(true); |
|
|
|
addMessage('WebSocket 连接已建立', true); |
|
|
|
addMessage('连接成功', false, 'system'); |
|
|
|
reconnectAttempts = 0; |
|
|
|
}; |
|
|
|
|
|
|
|
socket.onclose = function(event) { |
|
|
|
|
|
|
|
socket.onclose = function() { |
|
|
|
updateStatus(false); |
|
|
|
addMessage(`WebSocket 连接已关闭 (${event.code})`, true); |
|
|
|
addMessage('连接已关闭', false, 'system'); |
|
|
|
attemptReconnect(); |
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
socket.onerror = function(error) { |
|
|
|
addMessage(`连接错误: ${error.message || '未知错误'}`, true); |
|
|
|
addMessage(`连接错误: ${error.message}`, false, 'error'); |
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
socket.onmessage = function(event) { |
|
|
|
addMessage(event.data, true); |
|
|
|
try { |
|
|
|
const data = JSON.parse(event.data); |
|
|
|
addMessage(data.content, true, data.type || 'message'); |
|
|
|
} catch (e) { |
|
|
|
addMessage(event.data, true); |
|
|
|
} |
|
|
|
}; |
|
|
|
} catch (error) { |
|
|
|
addMessage(`连接失败: ${error.message}`, true); |
|
|
|
addMessage(`连接失败: ${error.message}`, false, 'error'); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
// 按钮事件处理 |
|
|
|
connectBtn.addEventListener('click', function() { |
|
|
|
if (!socket || socket.readyState === WebSocket.CLOSED) { |
|
|
|
initWebSocket(); |
|
|
|
// 尝试重新连接 |
|
|
|
function attemptReconnect() { |
|
|
|
if (reconnectAttempts < maxReconnectAttempts) { |
|
|
|
reconnectAttempts++; |
|
|
|
addMessage(`尝试重新连接 (${reconnectAttempts}/${maxReconnectAttempts})...`, false, 'system'); |
|
|
|
setTimeout(connect, reconnectDelay); |
|
|
|
} else { |
|
|
|
addMessage('达到最大重连次数,请手动重连', false, 'error'); |
|
|
|
} |
|
|
|
}); |
|
|
|
} |
|
|
|
|
|
|
|
disconnectBtn.addEventListener('click', function() { |
|
|
|
if (socket && socket.readyState === WebSocket.OPEN) { |
|
|
|
socket.close(); |
|
|
|
// 发送消息 |
|
|
|
function sendMessage() { |
|
|
|
if (!socket || socket.readyState !== WebSocket.OPEN) { |
|
|
|
addMessage('未连接到服务器', false, 'error'); |
|
|
|
return; |
|
|
|
} |
|
|
|
}); |
|
|
|
|
|
|
|
sendBtn.addEventListener('click', function() { |
|
|
|
if (socket && socket.readyState === WebSocket.OPEN) { |
|
|
|
const message = messageInput.value.trim(); |
|
|
|
if (message) { |
|
|
|
socket.send(message); |
|
|
|
addMessage(message); |
|
|
|
messageInput.value = ''; |
|
|
|
} |
|
|
|
} else { |
|
|
|
addMessage('WebSocket 未连接', true); |
|
|
|
const message = messageInput.value.trim(); |
|
|
|
if (!message) return; |
|
|
|
|
|
|
|
const messageType = messageTypeSelect.value; |
|
|
|
const data = { |
|
|
|
type: messageType, |
|
|
|
content: message, |
|
|
|
timestamp: new Date().toISOString() |
|
|
|
}; |
|
|
|
|
|
|
|
try { |
|
|
|
socket.send(JSON.stringify(data)); |
|
|
|
addMessage(message, false, messageType); |
|
|
|
messageInput.value = ''; |
|
|
|
} catch (error) { |
|
|
|
addMessage(`发送失败: ${error.message}`, false, 'error'); |
|
|
|
} |
|
|
|
}); |
|
|
|
} |
|
|
|
|
|
|
|
// 按回车发送消息 |
|
|
|
messageInput.addEventListener('keypress', function(e) { |
|
|
|
if (e.key === 'Enter') { |
|
|
|
sendBtn.click(); |
|
|
|
// 事件监听 |
|
|
|
connectBtn.addEventListener('click', connect); |
|
|
|
disconnectBtn.addEventListener('click', function() { |
|
|
|
if (socket) { |
|
|
|
socket.close(); |
|
|
|
} |
|
|
|
}); |
|
|
|
|
|
|
|
// 按回车连接WebSocket |
|
|
|
wsAddressInput.addEventListener('keypress', function(e) { |
|
|
|
sendBtn.addEventListener('click', sendMessage); |
|
|
|
messageInput.addEventListener('keypress', function(e) { |
|
|
|
if (e.key === 'Enter') { |
|
|
|
connectBtn.click(); |
|
|
|
sendMessage(); |
|
|
|
} |
|
|
|
}); |
|
|
|
|
|
|
|
// 清空消息 |
|
|
|
clearBtn.addEventListener('click', function() { |
|
|
|
messagesContainer.innerHTML = ''; |
|
|
|
messageCount = 0; |
|
|
|
messageCountElement.textContent = '0'; |
|
|
|
}); |
|
|
|
|
|
|
|
// 初始化Three.js场景 |
|
|
|
// 初始化Three.js |
|
|
|
initThreeJS(); |
|
|
|
</script> |
|
|
|
</body> |
|
|
|