上篇: AI实现在线聊天室--06 这篇在上篇基础上添加私聊的功能
主要修改点
- 前端:
- 在用户列表中添加私聊按钮
- 实现私聊对话框切换
- 修改消息显示逻辑以区分群聊和私聊
- 后端:
- 添加私聊消息处理逻辑
- 修改广播函数以支持定向发送
效果图预览
完整实现代码
1. 服务端代码 (server.js)
const WebSocket = require('ws');
const http = require('http');
// 创建HTTP服务器
const server = http.createServer();
const wss = new WebSocket.Server({ server });
// 存储所有连接的客户端和用户信息
const clients = new Map();
wss.on('connection', (ws) => {
console.log('New client connected');
// 为新连接分配一个默认用户名
const defaultUsername = `User${Math.floor(Math.random() * 1000)}`;
const userColor = getRandomColor();
clients.set(ws, {
username: defaultUsername,
color: userColor,
id: generateUserId()
});
// 发送初始用户信息
ws.send(JSON.stringify({
type: 'userInfo',
userId: clients.get(ws).id,
username: defaultUsername,
color: userColor
}));
// 通知所有客户端有新用户加入
broadcastUserList();
ws.on('message', (message) => {
try {
const data = JSON.parse(message);
if (data.type === 'setUsername') {
// 更新用户名
const userData = clients.get(ws);
userData.username = data.username;
userData.color = data.color || userColor;
clients.set(ws, userData);
broadcastUserList();
// 发送欢迎消息(不发送给新加入的用户自己)
const welcomeMsg = {
type: 'message',
username: '系统',
content: `${data.username} 加入了聊天室`,
timestamp: new Date().toISOString(),
color: '#888'
};
broadcast(JSON.stringify(welcomeMsg), ws);
} else if (data.type === 'message') {
const userData = clients.get(ws);
if (!userData) return;
const msg = {
type: 'message',
username: userData.username,
content: data.content,
timestamp: new Date().toISOString(),
color: userData.color,
target: data.target // 添加目标字段(null表示群聊)
};
if (data.target) {
// 私聊消息:只发送给目标用户和发送者
const targetClient = Array.from(clients.keys()).find(client => {
const clientData = clients.get(client);
return clientData.username === data.target;
});
if (targetClient && targetClient.readyState === WebSocket.OPEN) {
targetClient.send(JSON.stringify(msg));
}
// 发送者自己也需要收到消息(前端会过滤)
// ws.send(JSON.stringify(msg));
} else {
// 群聊消息:广播给除发送者外的所有人
broadcast(JSON.stringify(msg), ws);
}
} else if (data.type === 'image') {
// 图片消息处理
const userData = clients.get(ws);
if (!userData) return;
// 检查图片大小(限制为6MB,Base64会比原文件大约1.37倍)
if (data.imageData.length > 8 * 1024 * 1024) {
ws.send(JSON.stringify({
type: 'message',
username: '系统',
content: '图片太大,发送失败',
timestamp: new Date().toISOString(),
color: '#f00'
}));
return;
}
const msg = {
type: 'image',
username: userData.username,
imageData: data.imageData,
timestamp: new Date().toISOString(),
color: userData.color
};
broadcast(JSON.stringify(msg), ws);
}
} catch (e) {
console.error('Error parsing message:', e);
}
});
ws.on('close', () => {
console.log('Client disconnected');
const userData = clients.get(ws);
if (userData) {
// 通知所有客户端有用户离开(不发送给已断开的客户端)
const leaveMsg = {
type: 'message',
username: '系统',
content: `${userData.username} 离开了聊天室`,
timestamp: new Date().toISOString(),
color: '#888'
};
broadcast(JSON.stringify(leaveMsg), ws);
clients.delete(ws);
broadcastUserList();
}
});
});
// 广播消息给所有客户端(可排除特定客户端)
function broadcast(message, excludeSocket = null) {
wss.clients.forEach(client => {
if (client !== excludeSocket && client.readyState === WebSocket.OPEN) {
client.send(message);
}
});
}
// 广播用户列表给所有客户端
function broadcastUserList() {
const userList = Array.from(clients.values()).map(user => ({
id: user.id,
username: user.username,
color: user.color
}));
const message = {
type: 'userList',
users: userList
};
broadcast(JSON.stringify(message));
}
// 生成随机用户ID
function generateUserId() {
return Math.random().toString(36).substr(2, 9);
}
// 生成随机颜色
function getRandomColor() {
const letters = '0123456789ABCDEF';
let color = '#';
for (let i = 0; i < 6; i++) {
color += letters[Math.floor(Math.random() * 16)];
}
return color;
}
// 启动服务器
const PORT = process.env.PORT || 6002;
server.listen(PORT, () => {
console.log(`Server is running on port ${PORT}`);
});
2. 客户端端代码 (index2.html、socket.js、message.js、message-input.js)
index2.html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Modern WebSocket 聊天室</title>
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css">
<link rel="stylesheet" href="./assets/css/main.css">
<script type="text/javascript" src="./assets/js/socket.js"></script>
<script type="text/javascript" src="./assets/js/message.js"></script>
<script type="text/javascript" src="./assets/js/message-input.js"></script>
</head>
<body>
<div class="container">
<div class="chat-app">
<!-- 侧边栏 -->
<div class="sidebar">
<div class="app-header">
<div class="app-logo">
<i class="fas fa-comments"></i>
<span>Modern Chat</span>
</div>
</div>
<div class="user-profile">
<div class="avatar" id="user-avatar">U</div>
<div class="user-info">
<div class="username" id="sidebar-username">未设置用户名</div>
<div class="user-status">
<span class="status-dot"></span>
<span id="connection-status">离线</span>
</div>
</div>
</div>
<!-- 添加群聊按钮 -->
<div class="group-chat-btn active" id="group-chat-btn">
<i class="fas fa-users"></i>
<span>群聊</span>
</div>
<div class="user-list-title">其他在线用户 (<span id="online-count">0</span>)</div>
<div class="user-list" id="user-list-container">
<!-- 用户列表将通过JavaScript动态生成 -->
</div>
</div>
<!-- 主聊天区域 -->
<div class="chat-area">
<div class="chat-header">
<div class="chat-title" id="chat-title">群聊</div>
<div class="chat-mode-indicator" id="chat-mode-indicator">所有人可见</div>
</div>
<div class="messages-container" id="messages-container">
<!-- 消息将通过JavaScript动态生成 -->
<div class="message system-message">
<div class="message-text">欢迎来到Modern Chat!请设置您的用户名开始聊天。</div>
</div>
</div>
<div class="input-area">
<!-- <input type="text" class="message-input" id="message-input" placeholder="输入消息..." disabled> -->
<div class="message-input" id="message-input" contenteditable="true" placeholder="输入消息..."></div>
<label for="file-input" class="file-button" title="发送图片">
<i class="fas fa-image"></i>
</label>
<input type="file" id="file-input" accept="image/*" style="display: none;"
onchange="imageSelect(event)">
<button class="send-button" id="send-button" disabled>
<i class="fas fa-paper-plane"></i>
</button>
</div>
</div>
</div>
</div>
<!-- 用户名设置弹窗 -->
<div class="modal-overlay" id="username-modal">
<div class="modal">
<div class="modal-header">
<div class="modal-title">欢迎来到Modern Chat</div>
<div class="modal-subtitle">请设置您的用户名以开始聊天</div>
</div>
<div class="form-group">
<input type="text" class="form-input" id="username-input" placeholder="输入用户名" autofocus>
</div>
<button class="submit-button" id="set-username">开始聊天</button>
</div>
</div>
<script>
// 连接状态管理
let socket = null;
let isConnected = false;
let isConnecting = false;
let reconnectAttempts = 0;
const MAX_RECONNECT_ATTEMPTS = 5;
// 用户信息
let username = '';
let userColor = '';
// 在原有变量基础上添加以下内容
let currentChatMode = 'group'; // 'group' 或 'private'
let currentTarget = null; // 私聊目标用户名
const chatMessages = {
group: [], // 群聊消息
private: {} // 私聊消息 { username: [messages] }
};
// DOM元素
const messagesContainer = document.getElementById('messages-container');
const messageInput = document.getElementById('message-input');
const sendButton = document.getElementById('send-button');
const usernameInput = document.getElementById('username-input');
const setUsernameButton = document.getElementById('set-username');
const usernameModal = document.getElementById('username-modal');
const userListContainer = document.getElementById('user-list-container');
const sidebarUsername = document.getElementById('sidebar-username');
const userAvatar = document.getElementById('user-avatar');
const connectionStatus = document.getElementById('connection-status');
const onlineCount = document.getElementById('online-count');
// 初始化
document.addEventListener('DOMContentLoaded', () => {
initWebSocket();
initInputBox('message-input');
// 确保事件监听器只绑定一次
setUsernameButton.onclick = setUsername;
usernameInput.onkeypress = (e) => {
if (e.key === 'Enter') setUsername();
};
sendButton.onclick = sendMessage;
messageInput.onkeypress = (e) => {
if (e.key === 'Enter') sendMessage();
};
messagesContainer.onclick = (e) => {
const img = e.target.closest('.message-image');
if (img) {
// 直接从src属性获取数据
showFullImage(img.src);
}
}
// 初始化群聊按钮状态
document.getElementById('group-chat-btn').classList.add('active');
});
</script>
</body>
</html>
main.css
:root {
--primary-color: #4361ee;
--secondary-color: #3f37c9;
--accent-color: #4895ef;
--dark-color: #2b2d42;
--light-color: #f8f9fa;
--success-color: #4cc9f0;
--warning-color: #f72585;
--gray-color: #adb5bd;
--light-gray: #e9ecef;
--message-bg: #ffffff;
--own-message-bg: #e3f2fd;
--system-message-bg: #f8f9fa;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Roboto', sans-serif;
background-color: #f5f7fa;
color: var(--dark-color);
line-height: 1.6;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
.chat-app {
display: flex;
height: 90vh;
background-color: white;
border-radius: 16px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
overflow: hidden;
}
/* 侧边栏样式 */
.sidebar {
width: 300px;
background-color: var(--dark-color);
color: white;
padding: 20px;
display: flex;
flex-direction: column;
}
.app-header {
display: flex;
align-items: center;
margin-bottom: 30px;
}
.app-logo {
font-size: 24px;
font-weight: 700;
color: white;
display: flex;
align-items: center;
}
.app-logo i {
margin-right: 10px;
color: var(--accent-color);
}
.app-logo span {
white-space: nowrap;
}
.user-profile {
display: flex;
align-items: center;
margin-bottom: 30px;
padding-bottom: 20px;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.avatar {
width: 50px;
height: 50px;
border-radius: 50%;
background-color: var(--accent-color);
display: flex;
align-items: center;
justify-content: center;
margin-right: 15px;
font-weight: bold;
font-size: 20px;
}
.user-info {
flex-grow: 1;
}
.username {
font-weight: 500;
margin-bottom: 5px;
}
.user-status {
font-size: 12px;
color: var(--gray-color);
display: flex;
align-items: center;
}
.status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background-color: var(--success-color);
margin-right: 5px;
}
.user-list-title {
font-size: 14px;
text-transform: uppercase;
letter-spacing: 1px;
color: var(--gray-color);
margin-bottom: 15px;
}
.user-list {
flex-grow: 1;
overflow-y: auto;
}
.user-item {
display: flex;
align-items: center;
padding: 10px 0;
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
cursor: pointer;
transition: all 0.3s ease;
}
.user-item:hover {
background-color: rgba(255, 255, 255, 0.05);
border-radius: 8px;
padding: 10px;
}
.user-item .avatar {
width: 40px;
height: 40px;
font-size: 16px;
}
.user-item .username {
font-size: 14px;
margin-bottom: 0;
}
/* 主聊天区域 */
.chat-area {
flex-grow: 1;
display: flex;
flex-direction: column;
}
.chat-header {
padding: 20px;
border-bottom: 1px solid var(--light-gray);
display: flex;
align-items: center;
}
.chat-title {
font-weight: 500;
font-size: 18px;
}
.messages-container {
flex-grow: 1;
padding: 20px;
overflow-y: auto;
background-color: var(--light-color);
}
.message {
display: flex;
margin-bottom: 20px;
animation: fadeIn 0.3s ease;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.message-avatar {
width: 40px;
height: 40px;
border-radius: 50%;
background-color: var(--accent-color);
display: flex;
align-items: center;
justify-content: center;
margin-right: 15px;
font-weight: bold;
color: white;
flex-shrink: 0;
}
.message-content {
max-width: 70%;
}
.message-info {
display: flex;
align-items: center;
justify-content: flex-end;
margin-bottom: 5px;
}
.message-username {
font-weight: 500;
margin-right: 10px;
}
.message-time {
font-size: 12px;
color: var(--gray-color);
}
.message-text {
padding: 12px 16px;
border-radius: 18px;
background-color: var(--message-bg);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
position: relative;
word-break: break-word;
}
.message-text:after {
content: '';
position: absolute;
left: -8px;
top: 12px;
width: 0;
height: 0;
border-top: 8px solid transparent;
border-bottom: 8px solid transparent;
border-right: 8px solid var(--message-bg);
}
.message-text img {
width: 100%;
}
.own-message {
justify-content: flex-end;
}
.own-message .message-content {
order: -1;
margin-right: 15px;
}
.own-message .message-text {
background-color: var(--own-message-bg);
}
.own-message .message-text:after {
left: auto;
right: -8px;
border-right: none;
border-left: 8px solid var(--own-message-bg);
}
.system-message {
justify-content: center;
}
.system-message .message-text {
background-color: var(--system-message-bg);
color: var(--gray-color);
font-size: 13px;
padding: 8px 16px;
}
.system-message .message-text:after {
display: none;
}
.input-area {
padding: 15px 20px;
border-top: 1px solid var(--light-gray);
display: flex;
align-items: center;
}
.message-input {
flex-grow: 1;
min-height: 100px;
padding: 12px 16px;
border: 1px solid var(--light-gray);
border-radius: 24px;
outline: none;
font-size: 14px;
transition: all 0.3s ease;
}
.message-input:focus {
border-color: var(--accent-color);
box-shadow: 0 0 0 2px rgba(72, 149, 239, 0.2);
}
.message-input img {
max-width: 100%;
max-height: 200px;
display: block;
margin: 5px 0;
}
.send-button {
width: 48px;
height: 48px;
border-radius: 50%;
background-color: var(--primary-color);
color: white;
border: none;
margin-left: 10px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s ease;
}
.send-button:hover {
background-color: var(--secondary-color);
transform: translateY(-2px);
}
.send-button:disabled {
background-color: var(--gray-color);
cursor: not-allowed;
transform: none;
}
/* 添加照片按钮样式 */
.file-button {
width: 48px;
height: 48px;
border-radius: 50%;
background-color: var(--accent-color);
color: white;
display: flex;
align-items: center;
justify-content: center;
margin-left: 10px;
cursor: pointer;
transition: all 0.3s ease;
}
.file-button:hover {
background-color: var(--secondary-color);
transform: translateY(-2px);
}
/* 图片消息样式 */
.message-image {
max-width: 300px;
max-height: 300px;
border-radius: 12px;
margin-top: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
cursor: zoom-in;
transition: transform 0.3s ease;
}
.message-image:hover {
transform: scale(1.03);
}
/* 图片预览 */
.image-container {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.8);
display: flex;
align-items: center;
justify-content: center;
z-index: 2000;
cursor: zoom-out;
}
.image-container img {
max-width: 90%;
max-height: 90%;
object-fit: contain;
}
/* 用户名设置弹窗 */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.modal {
background-color: white;
border-radius: 12px;
padding: 30px;
width: 100%;
max-width: 400px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
animation: modalFadeIn 0.3s ease;
}
@keyframes modalFadeIn {
from {
opacity: 0;
transform: translateY(-20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.modal-header {
margin-bottom: 20px;
text-align: center;
}
.modal-title {
font-size: 24px;
font-weight: 500;
color: var(--dark-color);
margin-bottom: 10px;
}
.modal-subtitle {
color: var(--gray-color);
font-size: 14px;
}
.form-group {
margin-bottom: 20px;
}
.form-input {
width: 100%;
padding: 12px 16px;
border: 1px solid var(--light-gray);
border-radius: 8px;
font-size: 16px;
transition: all 0.3s ease;
}
.form-input:focus {
border-color: var(--accent-color);
box-shadow: 0 0 0 2px rgba(72, 149, 239, 0.2);
}
.submit-button {
width: 100%;
padding: 14px;
background-color: var(--primary-color);
color: white;
border: none;
border-radius: 8px;
font-size: 16px;
font-weight: 500;
cursor: pointer;
transition: all 0.3s ease;
}
.submit-button:hover {
background-color: var(--secondary-color);
}
/* 响应式设计 */
@media (max-width: 768px) {
.chat-app {
flex-direction: column;
height: 100vh;
border-radius: 0;
}
.sidebar {
width: 100%;
height: auto;
}
.user-list {
display: none;
}
.message-content {
max-width: 80%;
}
}
.group-chat-btn {
display: flex;
align-items: center;
padding: 10px;
margin-bottom: 15px;
border-radius: 8px;
cursor: pointer;
transition: all 0.3s ease;
background-color: rgba(255, 255, 255, 0.05);
}
.group-chat-btn:hover {
background-color: rgba(255, 255, 255, 0.1);
}
.group-chat-btn.active {
background-color: rgba(67, 97, 238, 0.2);
}
.group-chat-btn i {
margin-right: 10px;
color: var(--accent-color);
}
.user-item.active {
background-color: rgba(67, 97, 238, 0.2);
}
.chat-mode-indicator {
font-size: 12px;
color: var(--gray-color);
margin-top: 5px;
}
socket.js
// 初始化WebSocket连接
function initWebSocket() {
if (socket && (socket.readyState === WebSocket.OPEN || socket.readyState === WebSocket.CONNECTING)) {
return; // 如果已有活跃连接或正在连接,则不再创建新连接
}
isConnecting = true;
socket = new WebSocket('ws://localhost:6002');
// 清除旧的事件监听器(如果存在)
socket.onopen = null;
socket.onmessage = null;
socket.onclose = null;
socket.onerror = null;
socket.onopen = (event) => {
console.log('Connected to WebSocket server');
isConnected = true;
isConnecting = false;
reconnectAttempts = 0;
connectionStatus.textContent = '在线';
// 如果已有用户名,重新发送用户信息
if (username) {
socket.send(JSON.stringify({
type: 'setUsername',
username: username,
color: userColor
}));
}
};
socket.onmessage = (event) => {
const data = JSON.parse(event.data);
if (data.type === 'message') {
// 存储消息
if (data.target) {
// 私聊消息
const chatKey = `private_${data.username}`; // 发送者作为key
if (!chatMessages.private[chatKey]) {
chatMessages.private[chatKey] = [];
}
chatMessages.private[chatKey].push(data);
// 如果是当前正在查看的私聊,则显示消息
if (currentChatMode === 'private' && currentTarget === data.username) {
addMessage(data);
}
} else {
// 群聊消息
chatMessages.group.push(data);
if (currentChatMode === 'group') {
addMessage(data);
}
}
} else if (data.type === 'image') {
addMessage(data);
} else if (data.type === 'userList') {
updateUserList(data.users);
onlineCount.textContent = data.users.length > 0 ? data.users.length - 1 : data.users.length;
}
// else if (data.type === 'system') {
// addSystemMessage(data.content);
// }
};
socket.onclose = (event) => {
console.log('Disconnected from WebSocket server');
isConnected = false;
isConnecting = false;
connectionStatus.textContent = '离线';
addSystemMessage('与服务器的连接已断开');
// 禁用输入
messageInput.disabled = true;
sendButton.disabled = true;
// 尝试重新连接(带指数退避)
if (reconnectAttempts < MAX_RECONNECT_ATTEMPTS) {
const delay = Math.min(1000 * Math.pow(2, reconnectAttempts), 30000); // 最大30秒
reconnectAttempts++;
console.log(`尝试重新连接 (${reconnectAttempts}/${MAX_RECONNECT_ATTEMPTS})...`);
setTimeout(initWebSocket, delay);
} else {
addSystemMessage('无法连接到服务器,请刷新页面重试');
}
};
socket.onerror = (error) => {
console.error('WebSocket error:', error);
isConnecting = false;
};
}
message.js
// 显示完整图片
function showFullImage(imageData) {
// 检查是否已经存在大图查看器
const existingViewer = document.querySelector('.image-container');
if (existingViewer) {
document.body.removeChild(existingViewer);
}
const container = document.createElement('div');
container.className = 'image-container';
container.innerHTML = `
<img src="${imageData}" alt="完整图片">
`;
container.onclick = () => {
document.body.removeChild(container);
};
container.onkeypress = () => {
if (e.key === 'Escape') {
document.body.removeChild(container);
}
};
document.body.appendChild(container);
}
// 添加消息到聊天界面
function addMessage(data) {
const messageEl = document.createElement('div');
if (data.username === '系统') {
messageEl.className = 'message system-message';
messageEl.innerHTML = `
<div class="message-text">${data.content}</div>
`;
} else {
const isOwnMessage = data.username === username;
messageEl.className = `message ${isOwnMessage ? 'own-message' : ''}`;
const avatarText = data.username.charAt(0).toUpperCase();
const date = formatDate(data.timestamp);
const time = formatTime(data.timestamp);
let contentHtml = '';
if (data.type === 'image') {
contentHtml = `
<img src="${data.imageData}" class="message-image" alt="发送的图片">
`;
} else {
contentHtml = data.content;
}
messageEl.innerHTML = `
<div class="message-avatar" style="background-color: ${data.color}">${avatarText}</div>
<div class="message-content">
<div class="message-info">
<div class="message-username" style="color: ${data.color}">${data.username}</div>
<div class="message-time">${date} ${time}</div>
</div>
<div class="message-text">${contentHtml}</div>
</div>
`;
}
messagesContainer.appendChild(messageEl);
messagesContainer.scrollTop = messagesContainer.scrollHeight;
}
// 添加系统消息
function addSystemMessage(text) {
const messageEl = document.createElement('div');
messageEl.className = 'message system-message';
messageEl.innerHTML = `
<div class="message-text">${text}</div>
`;
messagesContainer.appendChild(messageEl);
messagesContainer.scrollTop = messagesContainer.scrollHeight;
}
// 更新用户列表
function updateUserList(users) {
userListContainer.innerHTML = '';
// 添加群聊按钮事件
const groupChatBtn = document.getElementById('group-chat-btn');
groupChatBtn.onclick = switchToGroupChat;
users.forEach(user => {
if (user.username === username) return; // 不显示自己
const userEl = document.createElement('div');
userEl.className = `user-item ${currentChatMode === 'private' && currentTarget === user.username ? 'active' : ''}`;
userEl.innerHTML = `
<div class="avatar" style="background-color: ${user.color}">${user.username.charAt(0).toUpperCase()}</div>
<div class="user-info">
<div class="username">${user.username}</div>
</div>
`;
userEl.onclick = () => {
switchToPrivateChat(user.username);
// 更新活跃状态
document.querySelectorAll('.user-item').forEach(item => {
item.classList.remove('active');
});
userEl.classList.add('active');
groupChatBtn.classList.remove('active');
};
userListContainer.appendChild(userEl);
});
}
// 格式化日期
function formatDate(timeStamp) {
const date = new Date(timeStamp);
return date.toLocaleDateString();
}
// 格式化时间
function formatTime(timestamp) {
const date = new Date(timestamp);
return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
}
// 生成随机颜色
function getRandomColor() {
const colors = [
'#4361ee', '#3f37c9', '#4895ef', '#4cc9f0',
'#7209b7', '#b5179e', '#f72585', '#560bad',
'#3a0ca3', '#480ca8', '#3a5a40', '#588157'
];
return colors[Math.floor(Math.random() * colors.length)];
}
// 设置用户名
function setUsername() {
const newUsername = usernameInput.value.trim();
if (newUsername) {
username = newUsername;
userColor = getRandomColor();
// 更新UI
sidebarUsername.textContent = username;
userAvatar.textContent = username.charAt(0).toUpperCase();
userAvatar.style.backgroundColor = userColor;
// 发送用户名到服务器
if (socket && socket.readyState === WebSocket.OPEN) {
socket.send(JSON.stringify({
type: 'setUsername',
username: newUsername,
color: userColor
}));
}
// 关闭弹窗
usernameModal.style.display = 'none';
// 启用消息输入
messageInput.disabled = false;
sendButton.disabled = false;
messageInput.focus();
addSystemMessage(`您已设置用户名为: ${username}`);
}
}
// 发送消息
function sendMessage() {
// const message = messageInput.value.trim();
const message = messageInput.innerHTML.trim();
if (message && socket && socket.readyState === WebSocket.OPEN) {
// 先显示自己的消息(乐观更新)
const tempMessage = {
type: 'message',
username: username,
content: message,
timestamp: new Date().toISOString(),
color: userColor,
target: currentChatMode === 'private' ? currentTarget : null
};
// 存储消息
if (currentChatMode === 'private') {
const chatKey = `private_${currentTarget}`;
if (!chatMessages.private[chatKey]) {
chatMessages.private[chatKey] = [];
}
chatMessages.private[chatKey].push(tempMessage);
} else {
chatMessages.group.push(tempMessage);
}
addMessage(tempMessage);
try {
// 发送到服务器
socket.send(JSON.stringify({
type: 'message',
content: message,
target: currentChatMode === 'private' ? currentTarget : null
}));
// 清空输入框
// messageInput.value = '';
messageInput.innerHTML = '';
} catch (error) {
console.error('发送消息失败:', error);
addSystemMessage('消息发送失败,请重试');
}
}
}
// 添加切换聊天模式的函数
function switchToGroupChat() {
currentChatMode = 'group';
currentTarget = null;
document.getElementById('group-chat-btn').classList.add('active');
document.getElementById('chat-title').textContent = '群聊';
document.getElementById('chat-mode-indicator').textContent = '所有人可见';
// 清空当前消息显示
messagesContainer.innerHTML = '';
// 显示群聊消息
chatMessages.group.forEach(msg => {
addMessage(msg);
});
// 如果没有消息,显示系统提示
if (chatMessages.group.length === 0) {
addSystemMessage('欢迎来到群聊!');
}
}
function switchToPrivateChat(targetUsername) {
currentChatMode = 'private';
currentTarget = targetUsername;
document.getElementById('group-chat-btn').classList.remove('active');
document.getElementById('chat-title').textContent = `与 ${targetUsername} 的私聊`;
document.getElementById('chat-mode-indicator').textContent = '仅你们两人可见';
// 清空当前消息显示
messagesContainer.innerHTML = '';
// 显示私聊消息
const chatKey = `private_${targetUsername}`;
if (chatMessages.private[chatKey]) {
chatMessages.private[chatKey].forEach(msg => {
addMessage(msg);
});
} else {
addSystemMessage(`你开始与 ${targetUsername} 私聊`);
}
}
message-input.js
function initInputBox(eleId) {
const messageInput = document.getElementById(eleId);
messageInput
// 处理粘贴事件(主要是图片)
messageInput.addEventListener('paste', function (e) {
e.preventDefault();
// 检查剪贴板中是否有图片
const items = (e.clipboardData || e.originalEvent.clipboardData).items;
for (let i = 0; i < items.length; i++) {
if (items[i].type.indexOf('image') !== -1) {
const blob = items[i].getAsFile();
const reader = new FileReader();
reader.onload = function (event) {
// 创建img元素并插入到输入框
const img = document.createElement('img');
img.src = event.target.result;
img.className = 'message-image';
img.setAttribute('data-image', 'true');
// 插入到当前光标位置
const selection = window.getSelection();
if (selection.rangeCount > 0) {
const range = selection.getRangeAt(0);
range.deleteContents();
range.insertNode(img);
// 在图片后添加换行并聚焦
const br = document.createElement('br');
range.insertNode(br);
// 移动光标到新位置
range.setStartAfter(br);
range.collapse(true);
selection.removeAllRanges();
selection.addRange(range);
} else {
messageInput.appendChild(img);
messageInput.appendChild(document.createElement('br'));
}
};
reader.readAsDataURL(blob);
break; // 只处理第一个图片
}
}
// 处理文本粘贴
const text = (e.clipboardData || window.clipboardData).getData('text');
if (text && text.trim() !== '') {
document.execCommand('insertText', false, text);
}
});
messageInput.onkeypress = (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
sendMessage();
}
};
}
function imageSelect(e) {
const file = e.target.files[0];
if (!file) return;
if (!file.type.match('image.*')) {
addSystemMessage('只能发送图片文件');
return;
}
// 检查文件大小(限制为5MB)
if (file.size > 5 * 1024 * 1024) {
addSystemMessage('图片大小不能超过5MB');
return;
}
const reader = new FileReader();
reader.onload = (event) => {
const imageData = event.target.result;
// 如果是WebSocket连接状态良好
if (socket && socket.readyState === WebSocket.OPEN) {
// 先显示自己的图片消息(乐观更新)
const tempMessage = {
type: 'image',
username: username,
imageData: imageData,
timestamp: new Date().toISOString(),
color: userColor
};
addMessage(tempMessage);
try {
// 发送到服务器
socket.send(JSON.stringify({
type: 'image',
imageData: imageData,
fileName: file.name
}));
} catch (error) {
console.error('发送图片失败:', error);
addSystemMessage('消息图片发送失败,请重试');
}
}
};
reader.readAsDataURL(file);
// 重置input,允许重复选择同一文件
e.target.value = '';
}
功能说明
-
私聊功能:
- 点击用户私聊
- 独立的私聊消息显示
-
消息区分:
- 群聊消息显示在主聊天区域
- 私聊消息显示在私聊窗口
- 系统消息特殊显示
-
用户体验:
- 消息即时显示
- 支持多窗口同时私聊
-
连接管理:
- 自动重连机制
- 连接状态显示
- 错误处理和反馈