AI实现在线聊天室--05

66 阅读7分钟

上篇: AI实现在线聊天室--04(消息输入功能丰富的方案备选)这篇在上篇基础上做了消息输入的功能丰富,可以发送图片,后期再看需求支持MarkDown。并做了代码整理,按功能划分了文件。

1、当前文件目录结构

json

.
├── assets
│   ├── css
│   │   └── main.css
│   └── js
│       ├── message-input.js
│       ├── message.js
│       └── socket.js
├── index2.html
├── index.html

2、主要修改点

  1. 提取css文件

    • 将内联的CSS代码提取到独立文件main.css,也方便后期分功能分模块管理
  2. 提取js文件

    • 保留部分初始化代码
    • socket相关代码提取到socket.js
    • message相关代码提取到message.js
    • 消息输入框改造及相关代码提取到message-input.js

3、当前完整代码

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="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">群聊</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>
                    <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 = '';

        // 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;
        });
    </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;
}

/* 用户名设置弹窗 */
.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%;
    }
}

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') {
            addMessage(data);
        } else if (data.type === 'userList') {
            updateUserList(data.users);
            onlineCount.textContent = data.users.length > 0 ? data.users.length - 1 : data.users.length;
        }
    };

    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-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.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();
        }
    };
}

message.js

// 添加消息到聊天界面
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);

        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">${data.content}</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 = '';

    users.forEach(user => {
        if (user.username === username) return; // 不显示自己

        const userEl = document.createElement('div');
        userEl.className = 'user-item';
        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>
                `;
        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
        };
        addMessage(tempMessage);

        try {
            // 发送到服务器
            socket.send(JSON.stringify({
                type: 'message',
                content: message
            }));

            // 清空输入框
            // messageInput.value = '';
            messageInput.innerHTML = '';
        } catch (error) {
            console.error('发送消息失败:', error);
            addSystemMessage('消息发送失败,请重试');
        }
    }
}