hnu202326010206 4 months ago
parent e08f5aa532
commit 74027cfc7b

@ -0,0 +1,2 @@
{
}

@ -64,6 +64,8 @@ public class WebSocketServer {
return;
}
System.out.println("请求: " + firstLine);
// 读取所有请求头
Map<String, String> headers = new HashMap<>();
String line;
@ -76,13 +78,14 @@ public class WebSocketServer {
}
}
// 判断是HTTP请求还是WebSocket握手
if (firstLine.startsWith("GET / HTTP") && !headers.containsKey("Upgrade")) {
// 返回HTML页面
serveHtmlPage(socket);
} else if (headers.containsKey("Upgrade") && "websocket".equalsIgnoreCase(headers.get("Upgrade"))) {
// 判断请求类型
if (headers.containsKey("Upgrade") && "websocket".equalsIgnoreCase(headers.get("Upgrade"))) {
// WebSocket握手
performWebSocketHandshake(socket, headers);
} else if (firstLine.startsWith("GET")) {
// HTTP请求 - 提供静态文件
String path = firstLine.split(" ")[1];
serveStaticFile(socket, path);
} else {
socket.close();
}
@ -97,21 +100,77 @@ public class WebSocketServer {
}
}
private void serveHtmlPage(Socket socket) throws IOException {
private void serveStaticFile(Socket socket, String path) throws IOException {
OutputStream out = socket.getOutputStream();
String html = getHtmlContent();
// 默认路径
if (path.equals("/") || path.equals("")) {
path = "/index.html";
}
// 读取文件
String content = null;
String contentType = "text/html; charset=UTF-8";
try {
if (path.equals("/index.html")) {
content = readFile("web/index.html");
contentType = "text/html; charset=UTF-8";
} else if (path.equals("/style.css")) {
content = readFile("web/style.css");
contentType = "text/css; charset=UTF-8";
} else if (path.equals("/app.js")) {
content = readFile("web/app.js");
contentType = "application/javascript; charset=UTF-8";
} else {
// 404
content = "<html><body><h1>404 Not Found</h1></body></html>";
String response = "HTTP/1.1 404 Not Found\r\n" +
"Content-Type: text/html; charset=UTF-8\r\n" +
"Content-Length: " + content.getBytes(StandardCharsets.UTF_8).length + "\r\n" +
"\r\n" +
content;
out.write(response.getBytes(StandardCharsets.UTF_8));
out.flush();
socket.close();
return;
}
} catch (Exception e) {
System.err.println("读取文件失败: " + e.getMessage());
content = "<html><body><h1>500 Internal Server Error</h1></body></html>";
contentType = "text/html; charset=UTF-8";
}
// 发送响应
String response = "HTTP/1.1 200 OK\r\n" +
"Content-Type: text/html; charset=UTF-8\r\n" +
"Content-Length: " + html.getBytes(StandardCharsets.UTF_8).length + "\r\n" +
"Content-Type: " + contentType + "\r\n" +
"Content-Length: " + content.getBytes(StandardCharsets.UTF_8).length + "\r\n" +
"Cache-Control: no-cache\r\n" +
"\r\n" +
html;
content;
out.write(response.getBytes(StandardCharsets.UTF_8));
out.flush();
socket.close();
}
private String readFile(String filePath) throws IOException {
File file = new File(filePath);
if (!file.exists()) {
throw new IOException("文件不存在: " + filePath);
}
StringBuilder content = new StringBuilder();
try (BufferedReader reader = new BufferedReader(
new FileReader(file, StandardCharsets.UTF_8))) {
String line;
while ((line = reader.readLine()) != null) {
content.append(line).append("\n");
}
}
return content.toString();
}
private void performWebSocketHandshake(Socket socket, Map<String, String> headers) throws Exception {
String key = headers.get("Sec-WebSocket-Key");
@ -192,155 +251,6 @@ public class WebSocketServer {
}
}
private String getHtmlContent() {
return "<!DOCTYPE html>\n" +
"<html lang=\"zh-CN\">\n" +
"<head>\n" +
" <meta charset=\"UTF-8\">\n" +
" <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n" +
" <title>TCP点对点聊天系统</title>\n" +
" <style>\n" +
" * { margin: 0; padding: 0; box-sizing: border-box; }\n" +
" body { font-family: 'Segoe UI', Arial, sans-serif; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); height: 100vh; display: flex; justify-content: center; align-items: center; }\n" +
" .container { width: 90%; max-width: 1200px; height: 90vh; background: white; border-radius: 20px; box-shadow: 0 20px 60px rgba(0,0,0,0.3); display: flex; overflow: hidden; }\n" +
" .sidebar { width: 280px; background: #f8f9fa; border-right: 1px solid #e0e0e0; display: flex; flex-direction: column; }\n" +
" .sidebar-header { padding: 20px; background: #667eea; color: white; }\n" +
" .sidebar-header h2 { font-size: 18px; margin-bottom: 5px; }\n" +
" .sidebar-header .username { font-size: 14px; opacity: 0.9; }\n" +
" .user-list { flex: 1; overflow-y: auto; padding: 10px; }\n" +
" .user-item { padding: 12px 15px; margin: 5px 0; background: white; border-radius: 8px; cursor: pointer; transition: all 0.3s; display: flex; align-items: center; }\n" +
" .user-item:hover { background: #e3f2fd; transform: translateX(5px); }\n" +
" .user-item.active { background: #667eea; color: white; }\n" +
" .user-item .status { width: 8px; height: 8px; background: #4caf50; border-radius: 50%; margin-right: 10px; }\n" +
" .chat-area { flex: 1; display: flex; flex-direction: column; }\n" +
" .chat-header { padding: 20px; background: white; border-bottom: 1px solid #e0e0e0; }\n" +
" .chat-header h3 { font-size: 18px; color: #333; }\n" +
" .messages { flex: 1; padding: 20px; overflow-y: auto; background: #fafafa; }\n" +
" .message { margin: 15px 0; display: flex; animation: slideIn 0.3s; }\n" +
" .message.sent { justify-content: flex-end; }\n" +
" .message-content { max-width: 60%; padding: 12px 16px; border-radius: 18px; word-wrap: break-word; }\n" +
" .message.received .message-content { background: white; color: #333; border-bottom-left-radius: 4px; }\n" +
" .message.sent .message-content { background: #667eea; color: white; border-bottom-right-radius: 4px; }\n" +
" .message-info { font-size: 11px; margin-top: 5px; opacity: 0.7; }\n" +
" .input-area { padding: 20px; background: white; border-top: 1px solid #e0e0e0; display: flex; gap: 10px; }\n" +
" .input-area input { flex: 1; padding: 12px 16px; border: 1px solid #e0e0e0; border-radius: 25px; font-size: 14px; outline: none; }\n" +
" .input-area input:focus { border-color: #667eea; }\n" +
" .input-area button { padding: 12px 30px; background: #667eea; color: white; border: none; border-radius: 25px; cursor: pointer; font-size: 14px; transition: all 0.3s; }\n" +
" .input-area button:hover { background: #5568d3; transform: translateY(-2px); }\n" +
" .login-modal { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); display: flex; justify-content: center; align-items: center; z-index: 1000; }\n" +
" .login-box { background: white; padding: 40px; border-radius: 15px; box-shadow: 0 10px 40px rgba(0,0,0,0.3); text-align: center; min-width: 350px; }\n" +
" .login-box h2 { margin-bottom: 30px; color: #333; }\n" +
" .login-box input { width: 100%; padding: 12px; margin: 10px 0; border: 1px solid #e0e0e0; border-radius: 8px; font-size: 14px; }\n" +
" .login-box button { width: 100%; padding: 12px; margin-top: 20px; background: #667eea; color: white; border: none; border-radius: 8px; cursor: pointer; font-size: 16px; }\n" +
" .login-box button:hover { background: #5568d3; }\n" +
" .welcome-screen { flex: 1; display: flex; flex-direction: column; justify-content: center; align-items: center; color: #999; }\n" +
" .welcome-screen i { font-size: 80px; margin-bottom: 20px; }\n" +
" @keyframes slideIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }\n" +
" .hidden { display: none; }\n" +
" </style>\n" +
"</head>\n" +
"<body>\n" +
" <div class=\"login-modal\" id=\"loginModal\">\n" +
" <div class=\"login-box\">\n" +
" <h2>🚀 欢迎使用聊天系统</h2>\n" +
" <input type=\"text\" id=\"usernameInput\" placeholder=\"请输入用户名\" maxlength=\"20\">\n" +
" <button onclick=\"login()\">登录</button>\n" +
" </div>\n" +
" </div>\n" +
" <div class=\"container hidden\" id=\"chatContainer\">\n" +
" <div class=\"sidebar\">\n" +
" <div class=\"sidebar-header\">\n" +
" <h2>在线用户</h2>\n" +
" <div class=\"username\" id=\"currentUser\"></div>\n" +
" </div>\n" +
" <div class=\"user-list\" id=\"userList\"></div>\n" +
" </div>\n" +
" <div class=\"chat-area\">\n" +
" <div class=\"chat-header\">\n" +
" <h3 id=\"chatTitle\">选择用户开始聊天</h3>\n" +
" </div>\n" +
" <div class=\"messages\" id=\"messages\">\n" +
" <div class=\"welcome-screen\">\n" +
" <div style=\"font-size: 60px;\">💬</div>\n" +
" <p>选择左侧用户开始聊天</p>\n" +
" </div>\n" +
" </div>\n" +
" <div class=\"input-area\">\n" +
" <input type=\"text\" id=\"messageInput\" placeholder=\"输入消息...\" disabled>\n" +
" <button onclick=\"sendMessage()\" id=\"sendBtn\" disabled>发送</button>\n" +
" </div>\n" +
" </div>\n" +
" </div>\n" +
" <script>\n" +
" let ws, username, currentChat = null;\n" +
" function login() {\n" +
" username = document.getElementById('usernameInput').value.trim();\n" +
" if (!username) { alert('请输入用户名'); return; }\n" +
" ws = new WebSocket('ws://' + window.location.host);\n" +
" ws.onopen = () => {\n" +
" ws.send(JSON.stringify({ type: 'LOGIN', sender: username }));\n" +
" document.getElementById('loginModal').classList.add('hidden');\n" +
" document.getElementById('chatContainer').classList.remove('hidden');\n" +
" document.getElementById('currentUser').textContent = username;\n" +
" };\n" +
" ws.onmessage = (e) => {\n" +
" const msg = JSON.parse(e.data);\n" +
" if (msg.type === 'USER_LIST') updateUserList(msg.users);\n" +
" else if (msg.type === 'PRIVATE_MSG') displayMessage(msg);\n" +
" else if (msg.type === 'ERROR') alert(msg.content);\n" +
" };\n" +
" ws.onerror = () => alert('连接失败');\n" +
" ws.onclose = () => alert('连接已断开');\n" +
" }\n" +
" function updateUserList(users) {\n" +
" const list = document.getElementById('userList');\n" +
" list.innerHTML = '';\n" +
" users.filter(u => u !== username).forEach(user => {\n" +
" const div = document.createElement('div');\n" +
" div.className = 'user-item' + (user === currentChat ? ' active' : '');\n" +
" div.innerHTML = '<span class=\"status\"></span>' + user;\n" +
" div.onclick = () => selectUser(user);\n" +
" list.appendChild(div);\n" +
" });\n" +
" }\n" +
" function selectUser(user) {\n" +
" currentChat = user;\n" +
" document.getElementById('chatTitle').textContent = '与 ' + user + ' 聊天';\n" +
" document.getElementById('messageInput').disabled = false;\n" +
" document.getElementById('sendBtn').disabled = false;\n" +
" document.querySelectorAll('.user-item').forEach(item => {\n" +
" item.classList.toggle('active', item.textContent === user);\n" +
" });\n" +
" document.querySelector('.welcome-screen')?.remove();\n" +
" }\n" +
" function sendMessage() {\n" +
" const input = document.getElementById('messageInput');\n" +
" const content = input.value.trim();\n" +
" if (!content || !currentChat) return;\n" +
" ws.send(JSON.stringify({ type: 'PRIVATE_MSG', sender: username, receiver: currentChat, content }));\n" +
" displayMessage({ sender: username, content, timestamp: Date.now() }, true);\n" +
" input.value = '';\n" +
" }\n" +
" function displayMessage(msg, isSent = false) {\n" +
" if (!isSent && msg.sender !== currentChat && msg.receiver !== currentChat) return;\n" +
" const div = document.createElement('div');\n" +
" div.className = 'message ' + (isSent || msg.sender === username ? 'sent' : 'received');\n" +
" const time = new Date(msg.timestamp).toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' });\n" +
" div.innerHTML = '<div class=\"message-content\">' + msg.content + '<div class=\"message-info\">' + time + '</div></div>';\n" +
" document.getElementById('messages').appendChild(div);\n" +
" div.scrollIntoView({ behavior: 'smooth' });\n" +
" }\n" +
" document.getElementById('messageInput').addEventListener('keypress', (e) => {\n" +
" if (e.key === 'Enter') sendMessage();\n" +
" });\n" +
" document.getElementById('usernameInput').addEventListener('keypress', (e) => {\n" +
" if (e.key === 'Enter') login();\n" +
" });\n" +
" </script>\n" +
"</body>\n" +
"</html>";
}
public static void main(String[] args) {
WebSocketServer server = new WebSocketServer();
Runtime.getRuntime().addShutdownHook(new Thread(() -> {

@ -0,0 +1,196 @@
# Web客户端说明
## 文件结构
```
web/
├── index.html # 主HTML文件
├── style.css # 样式文件
├── app.js # JavaScript逻辑
└── README.md # 本文件
```
## 功能特性
### 界面设计
- ✨ 现代化渐变紫色背景
- 📱 完全响应式设计,支持移动端
- 🎨 优雅的消息气泡样式
- 💫 平滑动画效果
- 🌈 渐变按钮和交互效果
### 核心功能
- 🔐 用户登录(支持自定义服务器地址)
- 👥 实时在线用户列表
- 💬 点对点私聊
- ⏰ 消息时间戳
- 🔄 自动重连提示
- ⌨️ 回车键快速发送
- 🚪 退出登录功能
## 使用方法
### 1. 启动服务器
```bash
# Linux/openEuler
./quick_start.sh
# Windows
quick_start.bat
```
### 2. 打开浏览器
访问:`http://localhost:8080`
### 3. 登录
1. 输入用户名
2. (可选)输入服务器地址,默认为 `localhost:8080`
3. 点击登录或按回车键
### 4. 开始聊天
1. 在左侧用户列表中选择要聊天的用户
2. 在底部输入框输入消息
3. 点击发送按钮或按回车键发送
## 技术实现
### WebSocket通信
客户端使用WebSocket协议与服务器进行实时通信
```javascript
ws = new WebSocket('ws://localhost:8080');
```
### 消息格式
所有消息使用JSON格式
```json
{
"type": "PRIVATE_MSG",
"sender": "Alice",
"receiver": "Bob",
"content": "Hello!",
"timestamp": 1234567890
}
```
### 消息类型
- `LOGIN` - 用户登录
- `LOGOUT` - 用户登出
- `PRIVATE_MSG` - 私聊消息
- `USER_LIST` - 在线用户列表
- `ACK` - 服务器确认
- `ERROR` - 错误消息
## 响应式设计
### 桌面端 (>768px)
- 完整的侧边栏和聊天区域
- 宽敞的消息显示区域
- 大号按钮和输入框
### 平板端 (768px - 480px)
- 缩小的侧边栏
- 优化的消息宽度
- 适中的按钮尺寸
### 移动端 (<480px)
- 可折叠的侧边栏
- 全屏聊天体验
- 触摸优化的交互
## 浏览器兼容性
支持所有现代浏览器:
- ✅ Chrome 60+
- ✅ Firefox 55+
- ✅ Safari 11+
- ✅ Edge 79+
- ✅ Opera 47+
## 自定义配置
### 修改默认服务器地址
编辑 `app.js` 第4行
```javascript
let serverAddress = 'your-server:8080';
```
### 修改主题颜色
编辑 `style.css` 中的渐变色:
```css
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
```
### 修改最大消息长度
编辑 `index.html` 中的 `maxlength` 属性:
```html
<input ... maxlength="500">
```
## 安全注意事项
1. **生产环境使用HTTPS/WSS**
- 当前使用的是 `ws://`(非加密)
- 生产环境应使用 `wss://`(加密)
2. **输入验证**
- 客户端已实现基本的HTML转义
- 服务器端应进行额外验证
3. **XSS防护**
- 使用 `textContent` 而非 `innerHTML` 处理用户输入
- 实现了 `escapeHtml()` 函数
## 故障排除
### 连接失败
- 检查服务器是否启动
- 检查服务器地址是否正确
- 检查防火墙设置
### 无法发送消息
- 确认已选择聊天对象
- 检查WebSocket连接状态
- 查看浏览器控制台错误信息
### 消息不显示
- 刷新页面重新登录
- 检查浏览器控制台
- 确认对方在线
## 开发调试
打开浏览器开发者工具F12
- **Console** - 查看日志和错误
- **Network** - 查看WebSocket连接
- **Application** - 查看存储和缓存
## 未来扩展
可以添加的功能:
- 📁 文件传输
- 🖼️ 图片发送和预览
- 😊 表情符号支持
- 🔔 消息通知
- 💾 聊天记录保存
- 🎤 语音消息
- 📹 视频通话
## 许可证
MIT License

@ -0,0 +1,299 @@
// 全局变量
let ws = null;
let username = null;
let currentChat = null;
let serverAddress = 'localhost:8080';
// 登录函数
function login() {
const usernameInput = document.getElementById('usernameInput');
const serverInput = document.getElementById('serverInput');
username = usernameInput.value.trim();
if (!username) {
alert('请输入用户名');
usernameInput.focus();
return;
}
// 获取服务器地址
const serverValue = serverInput.value.trim();
if (serverValue) {
serverAddress = serverValue;
}
// 连接WebSocket
connectWebSocket();
}
// 连接WebSocket
function connectWebSocket() {
try {
// 构建WebSocket URL
const wsUrl = `ws://${serverAddress}`;
console.log('连接到:', wsUrl);
ws = new WebSocket(wsUrl);
ws.onopen = () => {
console.log('WebSocket连接成功');
// 发送登录消息
const loginMsg = {
type: 'LOGIN',
sender: username
};
ws.send(JSON.stringify(loginMsg));
// 切换到聊天界面
document.getElementById('loginModal').classList.add('hidden');
document.getElementById('chatContainer').classList.remove('hidden');
document.getElementById('currentUser').textContent = username;
};
ws.onmessage = (event) => {
console.log('收到消息:', event.data);
try {
const message = JSON.parse(event.data);
handleMessage(message);
} catch (e) {
console.error('解析消息失败:', e);
}
};
ws.onerror = (error) => {
console.error('WebSocket错误:', error);
alert('连接失败,请检查服务器地址是否正确');
};
ws.onclose = () => {
console.log('WebSocket连接关闭');
if (document.getElementById('chatContainer').classList.contains('hidden')) {
alert('无法连接到服务器');
} else {
alert('连接已断开');
logout();
}
};
} catch (e) {
console.error('创建WebSocket失败:', e);
alert('连接失败: ' + e.message);
}
}
// 处理接收到的消息
function handleMessage(message) {
switch (message.type) {
case 'ACK':
console.log('服务器确认:', message.content);
break;
case 'USER_LIST':
updateUserList(message.users);
break;
case 'PRIVATE_MSG':
displayMessage(message, false);
break;
case 'ERROR':
alert('错误: ' + message.content);
break;
default:
console.log('未知消息类型:', message.type);
}
}
// 更新用户列表
function updateUserList(users) {
const userList = document.getElementById('userList');
userList.innerHTML = '';
// 过滤掉自己
const otherUsers = users.filter(u => u !== username);
if (otherUsers.length === 0) {
userList.innerHTML = '<div class="empty-users">暂无其他用户在线</div>';
return;
}
otherUsers.forEach(user => {
const userItem = document.createElement('div');
userItem.className = 'user-item';
if (user === currentChat) {
userItem.classList.add('active');
}
userItem.innerHTML = `
<span class="status"></span>
<span>${escapeHtml(user)}</span>
`;
userItem.onclick = () => selectUser(user);
userList.appendChild(userItem);
});
}
// 选择聊天用户
function selectUser(user) {
currentChat = user;
// 更新标题
document.getElementById('chatTitle').textContent = `${user} 聊天`;
// 启用输入框
const messageInput = document.getElementById('messageInput');
const sendBtn = document.getElementById('sendBtn');
messageInput.disabled = false;
sendBtn.disabled = false;
messageInput.focus();
// 更新用户列表选中状态
document.querySelectorAll('.user-item').forEach(item => {
const itemText = item.textContent.trim();
item.classList.toggle('active', itemText === user);
});
// 移除欢迎屏幕
const welcomeScreen = document.querySelector('.welcome-screen');
if (welcomeScreen) {
welcomeScreen.remove();
}
}
// 发送消息
function sendMessage() {
const messageInput = document.getElementById('messageInput');
const content = messageInput.value.trim();
if (!content) {
return;
}
if (!currentChat) {
alert('请先选择聊天对象');
return;
}
if (!ws || ws.readyState !== WebSocket.OPEN) {
alert('连接已断开,请重新登录');
return;
}
// 发送消息到服务器
const message = {
type: 'PRIVATE_MSG',
sender: username,
receiver: currentChat,
content: content
};
ws.send(JSON.stringify(message));
// 显示发送的消息
displayMessage({
sender: username,
content: content,
timestamp: Date.now()
}, true);
// 清空输入框
messageInput.value = '';
messageInput.focus();
}
// 显示消息
function displayMessage(message, isSent) {
// 只显示与当前聊天对象相关的消息
if (!isSent && message.sender !== currentChat && message.receiver !== currentChat) {
return;
}
const messagesDiv = document.getElementById('messages');
// 创建消息元素
const messageDiv = document.createElement('div');
messageDiv.className = `message ${isSent ? 'sent' : 'received'}`;
const time = new Date(message.timestamp).toLocaleTimeString('zh-CN', {
hour: '2-digit',
minute: '2-digit'
});
messageDiv.innerHTML = `
<div class="message-content">
<div class="message-text">${escapeHtml(message.content)}</div>
<div class="message-info">${time}</div>
</div>
`;
messagesDiv.appendChild(messageDiv);
// 滚动到底部
messageDiv.scrollIntoView({ behavior: 'smooth', block: 'end' });
}
// 退出登录
function logout() {
if (ws && ws.readyState === WebSocket.OPEN) {
const logoutMsg = {
type: 'LOGOUT',
sender: username
};
ws.send(JSON.stringify(logoutMsg));
ws.close();
}
// 重置状态
ws = null;
username = null;
currentChat = null;
// 切换到登录界面
document.getElementById('chatContainer').classList.add('hidden');
document.getElementById('loginModal').classList.remove('hidden');
// 清空输入
document.getElementById('usernameInput').value = '';
document.getElementById('messageInput').value = '';
// 清空消息
const messagesDiv = document.getElementById('messages');
messagesDiv.innerHTML = `
<div class="welcome-screen">
<div class="welcome-icon">💬</div>
<p>选择左侧用户开始聊天</p>
</div>
`;
}
// HTML转义函数
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// 回车发送消息
document.addEventListener('DOMContentLoaded', () => {
const messageInput = document.getElementById('messageInput');
const usernameInput = document.getElementById('usernameInput');
messageInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
sendMessage();
}
});
usernameInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
e.preventDefault();
login();
}
});
// 自动聚焦用户名输入框
usernameInput.focus();
});

@ -0,0 +1,66 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>TCP点对点聊天系统</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<!-- 登录界面 -->
<div class="login-modal" id="loginModal">
<div class="login-box">
<h2>🚀 欢迎使用聊天系统</h2>
<p class="login-desc">请输入您的用户名开始聊天</p>
<input type="text" id="usernameInput" placeholder="请输入用户名" maxlength="20" autocomplete="off">
<input type="text" id="serverInput" placeholder="服务器地址 (默认: localhost:8080)" autocomplete="off">
<button onclick="login()">登录</button>
</div>
</div>
<!-- 聊天主界面 -->
<div class="container hidden" id="chatContainer">
<div class="sidebar">
<div class="sidebar-header">
<h2>💬 在线用户</h2>
<div class="current-user">
<span class="status-dot"></span>
<span id="currentUser"></span>
</div>
</div>
<div class="user-list" id="userList">
<div class="empty-users">暂无其他用户在线</div>
</div>
</div>
<div class="chat-area">
<div class="chat-header">
<h3 id="chatTitle">选择用户开始聊天</h3>
<button class="logout-btn" onclick="logout()">退出</button>
</div>
<div class="messages" id="messages">
<div class="welcome-screen">
<div class="welcome-icon">💬</div>
<p>选择左侧用户开始聊天</p>
</div>
</div>
<div class="input-area">
<input
type="text"
id="messageInput"
placeholder="输入消息..."
disabled
autocomplete="off"
maxlength="500">
<button onclick="sendMessage()" id="sendBtn" disabled>
<span>发送</span>
</button>
</div>
</div>
</div>
<script src="app.js"></script>
</body>
</html>

@ -0,0 +1,505 @@
/* 全局样式 */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Helvetica', 'Arial', sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
padding: 20px;
}
/* 登录界面 */
.login-modal {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
backdrop-filter: blur(5px);
}
.login-box {
background: white;
padding: 40px;
border-radius: 20px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
text-align: center;
min-width: 350px;
max-width: 90%;
animation: fadeInUp 0.5s;
}
.login-box h2 {
margin-bottom: 10px;
color: #333;
font-size: 24px;
}
.login-desc {
color: #666;
margin-bottom: 30px;
font-size: 14px;
}
.login-box input {
width: 100%;
padding: 14px 16px;
margin: 10px 0;
border: 2px solid #e0e0e0;
border-radius: 10px;
font-size: 14px;
transition: all 0.3s;
outline: none;
}
.login-box input:focus {
border-color: #667eea;
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
}
.login-box button {
width: 100%;
padding: 14px;
margin-top: 20px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
border-radius: 10px;
cursor: pointer;
font-size: 16px;
font-weight: 600;
transition: all 0.3s;
}
.login-box button:hover {
transform: translateY(-2px);
box-shadow: 0 10px 20px rgba(102, 126, 234, 0.3);
}
.login-box button:active {
transform: translateY(0);
}
/* 主容器 */
.container {
width: 100%;
max-width: 1400px;
height: 85vh;
min-height: 600px;
background: white;
border-radius: 20px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
display: flex;
overflow: hidden;
animation: fadeIn 0.5s;
}
/* 侧边栏 */
.sidebar {
width: 300px;
background: #f8f9fa;
border-right: 1px solid #e0e0e0;
display: flex;
flex-direction: column;
}
.sidebar-header {
padding: 25px 20px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.sidebar-header h2 {
font-size: 18px;
margin-bottom: 12px;
font-weight: 600;
}
.current-user {
display: flex;
align-items: center;
font-size: 14px;
opacity: 0.95;
padding: 8px 12px;
background: rgba(255, 255, 255, 0.2);
border-radius: 8px;
}
.status-dot {
width: 10px;
height: 10px;
background: #4caf50;
border-radius: 50%;
margin-right: 10px;
animation: pulse 2s infinite;
}
/* 用户列表 */
.user-list {
flex: 1;
overflow-y: auto;
padding: 15px;
}
.empty-users {
text-align: center;
color: #999;
padding: 40px 20px;
font-size: 14px;
}
.user-item {
padding: 14px 16px;
margin: 8px 0;
background: white;
border-radius: 12px;
cursor: pointer;
transition: all 0.3s;
display: flex;
align-items: center;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
}
.user-item:hover {
background: #e3f2fd;
transform: translateX(5px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
}
.user-item.active {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
}
.user-item .status {
width: 8px;
height: 8px;
background: #4caf50;
border-radius: 50%;
margin-right: 12px;
}
.user-item.active .status {
background: #fff;
}
/* 聊天区域 */
.chat-area {
flex: 1;
display: flex;
flex-direction: column;
background: #fafafa;
}
.chat-header {
padding: 20px 25px;
background: white;
border-bottom: 1px solid #e0e0e0;
display: flex;
justify-content: space-between;
align-items: center;
}
.chat-header h3 {
font-size: 18px;
color: #333;
font-weight: 600;
}
.logout-btn {
padding: 8px 20px;
background: #f44336;
color: white;
border: none;
border-radius: 8px;
cursor: pointer;
font-size: 14px;
transition: all 0.3s;
}
.logout-btn:hover {
background: #d32f2f;
transform: translateY(-2px);
}
/* 消息区域 */
.messages {
flex: 1;
padding: 25px;
overflow-y: auto;
display: flex;
flex-direction: column;
}
.welcome-screen {
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
color: #999;
}
.welcome-icon {
font-size: 80px;
margin-bottom: 20px;
animation: bounce 2s infinite;
}
.welcome-screen p {
font-size: 16px;
}
/* 消息样式 */
.message {
margin: 12px 0;
display: flex;
animation: slideIn 0.3s ease-out;
}
.message.sent {
justify-content: flex-end;
}
.message.received {
justify-content: flex-start;
}
.message-content {
max-width: 65%;
padding: 12px 16px;
border-radius: 18px;
word-wrap: break-word;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
position: relative;
}
.message.received .message-content {
background: white;
color: #333;
border-bottom-left-radius: 4px;
}
.message.sent .message-content {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border-bottom-right-radius: 4px;
}
.message-text {
font-size: 15px;
line-height: 1.5;
margin-bottom: 6px;
}
.message-info {
font-size: 11px;
opacity: 0.7;
text-align: right;
}
/* 输入区域 */
.input-area {
padding: 20px 25px;
background: white;
border-top: 1px solid #e0e0e0;
display: flex;
gap: 12px;
align-items: center;
}
.input-area input {
flex: 1;
padding: 14px 18px;
border: 2px solid #e0e0e0;
border-radius: 25px;
font-size: 15px;
outline: none;
transition: all 0.3s;
}
.input-area input:focus {
border-color: #667eea;
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
}
.input-area input:disabled {
background: #f5f5f5;
cursor: not-allowed;
}
.input-area button {
padding: 14px 32px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
border-radius: 25px;
cursor: pointer;
font-size: 15px;
font-weight: 600;
transition: all 0.3s;
white-space: nowrap;
}
.input-area button:hover:not(:disabled) {
transform: translateY(-2px);
box-shadow: 0 8px 16px rgba(102, 126, 234, 0.3);
}
.input-area button:active:not(:disabled) {
transform: translateY(0);
}
.input-area button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
/* 滚动条样式 */
.user-list::-webkit-scrollbar,
.messages::-webkit-scrollbar {
width: 6px;
}
.user-list::-webkit-scrollbar-track,
.messages::-webkit-scrollbar-track {
background: transparent;
}
.user-list::-webkit-scrollbar-thumb,
.messages::-webkit-scrollbar-thumb {
background: #ccc;
border-radius: 3px;
}
.user-list::-webkit-scrollbar-thumb:hover,
.messages::-webkit-scrollbar-thumb:hover {
background: #999;
}
/* 工具类 */
.hidden {
display: none !important;
}
/* 动画 */
@keyframes fadeIn {
from {
opacity: 0;
transform: scale(0.95);
}
to {
opacity: 1;
transform: scale(1);
}
}
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes slideIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes pulse {
0%, 100% {
opacity: 1;
}
50% {
opacity: 0.5;
}
}
@keyframes bounce {
0%, 100% {
transform: translateY(0);
}
50% {
transform: translateY(-10px);
}
}
/* 响应式设计 */
@media (max-width: 768px) {
body {
padding: 0;
}
.container {
height: 100vh;
border-radius: 0;
max-width: 100%;
}
.sidebar {
width: 250px;
}
.login-box {
min-width: 300px;
padding: 30px 25px;
}
.message-content {
max-width: 80%;
}
.input-area {
padding: 15px;
}
.input-area button {
padding: 14px 24px;
}
}
@media (max-width: 480px) {
.sidebar {
position: absolute;
left: -250px;
height: 100%;
z-index: 100;
transition: left 0.3s;
}
.sidebar.show {
left: 0;
}
.chat-header h3 {
font-size: 16px;
}
.logout-btn {
padding: 6px 16px;
font-size: 13px;
}
}
Loading…
Cancel
Save