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.

382 lines
15 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;
}
</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>
<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>
<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 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;
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');
// 更新状态显示
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');
} 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) {
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');
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' : '我';
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;
if (isReceived) {
messageContent.appendChild(avatar);
messageContent.appendChild(bubble);
} else {
messageContent.appendChild(bubble);
messageContent.appendChild(avatar);
}
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();
}
particleGeometry.attributes.position.needsUpdate = true;
particleGeometry.attributes.color.needsUpdate = true;
}
// 初始化 WebSocket 连接
function initWebSocket() {
const wsAddress = wsAddressInput.value.trim();
if (!wsAddress) {
addMessage('请输入WebSocket地址', true);
return;
}
try {
// 确保地址以 ws:// 开头
const normalizedAddress = wsAddress.startsWith('ws://')
? wsAddress
: `ws://${wsAddress}`;
socket = new WebSocket(normalizedAddress);
socket.onopen = function() {
updateStatus(true);
addMessage('WebSocket 连接已建立', true);
};
socket.onclose = function(event) {
updateStatus(false);
addMessage(`WebSocket 连接已关闭 (${event.code})`, true);
};
socket.onerror = function(error) {
addMessage(`连接错误: ${error.message || '未知错误'}`, true);
};
socket.onmessage = function(event) {
addMessage(event.data, true);
};
} catch (error) {
addMessage(`连接失败: ${error.message}`, true);
}
}
// 按钮事件处理
connectBtn.addEventListener('click', function() {
if (!socket || socket.readyState === WebSocket.CLOSED) {
initWebSocket();
}
});
disconnectBtn.addEventListener('click', function() {
if (socket && socket.readyState === WebSocket.OPEN) {
socket.close();
}
});
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);
}
});
// 按回车发送消息
messageInput.addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
sendBtn.click();
}
});
// 按回车连接WebSocket
wsAddressInput.addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
connectBtn.click();
}
});
// 清空消息
clearBtn.addEventListener('click', function() {
messagesContainer.innerHTML = '';
});
// 初始化Three.js场景
initThreeJS();
</script>
</body>
</html>