You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

422 lines
17 KiB

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>WebSocket 3D 聊天客户端</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<style>
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
@keyframes slideIn {
from { transform: translateY(20px); opacity: 0; }
to { transform: translateY(0); opacity: 1; }
}
.status-dot {
animation: pulse 2s infinite;
}
.message {
animation: slideIn 0.3s ease-out;
}
#three-container {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: -1;
opacity: 0.3;
}
.content-container {
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">
<!-- Three.js 容器 -->
<div id="three-container"></div>
<!-- 内容容器 -->
<div class="content-container flex flex-col h-full">
<!-- 顶部状态栏 -->
<div class="bg-white dark:bg-gray-800 shadow-sm">
<div class="max-w-4xl mx-auto px-4 py-3">
<!-- WebSocket地址输入区域 -->
<div class="flex items-center space-x-2 mb-3">
<input type="text"
id="wsAddress"
value="ws://localhost:5202/ws"
placeholder="输入WebSocket地址"
class="flex-1 px-4 py-2 border border-gray-300 dark:border-gray-600
rounded-lg focus:outline-none focus:ring-2 focus:ring-indigo-500
dark:bg-gray-700 dark:text-white transition-colors duration-200">
</div>
<div class="flex justify-between items-center">
<div class="flex items-center space-x-2">
<div id="status" class="flex items-center space-x-2">
<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"
class="px-4 py-2 bg-indigo-500 hover:bg-indigo-600 text-white rounded-lg shadow-sm
transition-colors duration-200">
连接
</button>
<button id="disconnectBtn"
class="px-4 py-2 bg-red-500 hover:bg-red-600 text-white rounded-lg shadow-sm
transition-colors duration-200">
断开
</button>
</div>
</div>
</div>
</div>
<!-- 消息区域 -->
<div class="flex-1 overflow-hidden">
<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>
<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">
<!-- 消息将在这里动态添加 -->
</div>
</div>
</div>
<!-- 输入区域 -->
<div class="bg-white dark:bg-gray-800 shadow-sm">
<div class="max-w-4xl mx-auto px-4 py-4">
<div class="flex space-x-2">
<input type="text"
id="messageInput"
placeholder="输入消息..."
class="flex-1 px-4 py-2 border border-gray-300 dark:border-gray-600
rounded-lg focus:outline-none focus:ring-2 focus:ring-indigo-500
dark:bg-gray-700 dark:text-white transition-colors duration-200">
<button id="sendBtn"
class="px-4 py-2 bg-indigo-500 hover:bg-indigo-600 text-white rounded-lg
shadow-sm transition-colors duration-200">
发送
</button>
</div>
</div>
</div>
</div>
<script>
// Three.js 场景初始化
let scene, camera, renderer, particles;
const particleCount = 1000;
const particleGeometry = new THREE.BufferGeometry();
const particlePositions = new Float32Array(particleCount * 3);
const particleColors = new Float32Array(particleCount * 3);
const particleVelocities = new Float32Array(particleCount * 3);
function initThreeJS() {
scene = new THREE.Scene();
camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
renderer.setSize(window.innerWidth, window.innerHeight);
document.getElementById('three-container').appendChild(renderer.domElement);
// 初始化粒子
for (let i = 0; i < particleCount; i++) {
particlePositions[i * 3] = (Math.random() - 0.5) * 20;
particlePositions[i * 3 + 1] = (Math.random() - 0.5) * 20;
particlePositions[i * 3 + 2] = (Math.random() - 0.5) * 20;
particleColors[i * 3] = Math.random();
particleColors[i * 3 + 1] = Math.random();
particleColors[i * 3 + 2] = Math.random();
particleVelocities[i * 3] = (Math.random() - 0.5) * 0.02;
particleVelocities[i * 3 + 1] = (Math.random() - 0.5) * 0.02;
particleVelocities[i * 3 + 2] = (Math.random() - 0.5) * 0.02;
}
particleGeometry.setAttribute('position', new THREE.BufferAttribute(particlePositions, 3));
particleGeometry.setAttribute('color', new THREE.BufferAttribute(particleColors, 3));
const particleMaterial = new THREE.PointsMaterial({
size: 0.1,
vertexColors: true,
transparent: true,
opacity: 0.8
});
particles = new THREE.Points(particleGeometry, particleMaterial);
scene.add(particles);
camera.position.z = 15;
// 添加环境光
const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
scene.add(ambientLight);
// 添加点光源
const pointLight = new THREE.PointLight(0xffffff, 1);
pointLight.position.set(10, 10, 10);
scene.add(pointLight);
animate();
}
function animate() {
requestAnimationFrame(animate);
// 更新粒子位置
const positions = particleGeometry.attributes.position.array;
for (let i = 0; i < particleCount; i++) {
positions[i * 3] += particleVelocities[i * 3];
positions[i * 3 + 1] += particleVelocities[i * 3 + 1];
positions[i * 3 + 2] += particleVelocities[i * 3 + 2];
// 边界检查
if (Math.abs(positions[i * 3]) > 10) particleVelocities[i * 3] *= -1;
if (Math.abs(positions[i * 3 + 1]) > 10) particleVelocities[i * 3 + 1] *= -1;
if (Math.abs(positions[i * 3 + 2]) > 10) particleVelocities[i * 3 + 2] *= -1;
}
particleGeometry.attributes.position.needsUpdate = true;
// 旋转场景
scene.rotation.y += 0.001;
renderer.render(scene, camera);
}
// 处理窗口大小变化
window.addEventListener('resize', function() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
// 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');
const messagesContainer = document.getElementById('messages');
const messageInput = document.getElementById('messageInput');
const connectBtn = document.getElementById('connectBtn');
const disconnectBtn = document.getElementById('disconnectBtn');
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) {
if (connected) {
statusDot.classList.remove('bg-red-500');
statusDot.classList.add('bg-green-500');
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');
statusText.textContent = '未连接';
statusText.classList.remove('text-green-600');
statusText.classList.add('text-gray-600');
}
}
// 添加消息到显示区域
function addMessage(message, isReceived = false, type = 'message') {
const messageElement = document.createElement('div');
messageElement.className = `message flex ${isReceived ? 'justify-start' : 'justify-end'}`;
const messageContent = document.createElement('div');
messageContent.className = `flex items-start space-x-2 max-w-[80%]`;
const avatar = document.createElement('div');
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');
let bubbleClass = 'p-3 rounded-lg shadow-sm';
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;
// 更新消息计数
messageCount++;
messageCountElement.textContent = messageCount;
}
// 连接WebSocket
function connect() {
try {
socket = new WebSocket(wsAddressInput.value);
socket.onopen = function() {
updateStatus(true);
addMessage('连接成功', false, 'system');
reconnectAttempts = 0;
};
socket.onclose = function() {
updateStatus(false);
addMessage('连接已关闭', false, 'system');
attemptReconnect();
};
socket.onerror = function(error) {
addMessage(`连接错误: ${error.message}`, false, 'error');
};
socket.onmessage = function(event) {
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}`, false, 'error');
}
}
// 尝试重新连接
function attemptReconnect() {
if (reconnectAttempts < maxReconnectAttempts) {
reconnectAttempts++;
addMessage(`尝试重新连接 (${reconnectAttempts}/${maxReconnectAttempts})...`, false, 'system');
setTimeout(connect, reconnectDelay);
} else {
addMessage('达到最大重连次数,请手动重连', false, 'error');
}
}
// 发送消息
function sendMessage() {
if (!socket || socket.readyState !== WebSocket.OPEN) {
addMessage('未连接到服务器', false, 'error');
return;
}
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');
}
}
// 事件监听
connectBtn.addEventListener('click', connect);
disconnectBtn.addEventListener('click', function() {
if (socket) {
socket.close();
}
});
sendBtn.addEventListener('click', sendMessage);
messageInput.addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
sendMessage();
}
});
clearBtn.addEventListener('click', function() {
messagesContainer.innerHTML = '';
messageCount = 0;
messageCountElement.textContent = '0';
});
// 初始化Three.js
initThreeJS();
</script>
</body>
</html>