聊天室插件
Этот скрипт недоступен для установки пользователем. Он является библиотекой, которая подключается к другим скриптам мета-ключом // @require https://update.greasyfork.org/scripts/528523/1545641/Chat_Room.js
// ==UserScript== // @run-at document-start // @name Chat_Room // @description 聊天室插件 // @version 1.0.0 // @require https://code.jquery.com/jquery-3.6.0.min.js // ==/UserScript== class ChatRoom { constructor(config = {}) { // 配置项 this.config = { wsServer: config.wsServer || 'wss://topurl.cn:9001', authToken: config.authToken || window.btoa(encodeURIComponent('https://news.topurl.cn/')), maxHistory: config.maxHistory || 300, ...config }; // 加密配置 this.PREFIX = '🔒'; this.CHINESE_RANGE = { start: 0x4E00, end: 0x9FA5 }; this.HALL_DOMAIN = "square.io"; // 全局变量 this.activeWebsockets = {}; this.activeDomain = null; this.domainData = {}; this.isSending = false; this.isReconnecting = false; this.autoScroll = true; this.heartbeatTimer = null; this.userId = null; this.userName = null; this.domainList = []; this.showAllDomains = false; this.onlineUsers = null; // DOM 元素缓存 this.elements = {}; // 认证字符 this.authChar = this.config.authToken[1] + this.config.authToken[3] + this.config.authToken[7] + this.config.authToken[9]; } // 加密相关方法 encrypt(text) { try { const encrypted = this.compressEncrypt(text); return this.PREFIX + encrypted; } catch (e) { console.error('加密失败:', e); return text; } } decrypt(text) { if (!text.startsWith(this.PREFIX)) return text; try { const encryptedText = text.slice(this.PREFIX.length); return this.compressDecrypt(encryptedText); } catch (e) { console.error('解密失败:', e); return text; } } compressEncrypt(text) { const mapStart = this.CHINESE_RANGE.start; const bytes = new TextEncoder().encode(text); let encrypted = ''; // 添加长度标记确保解密精确 const lengthMark = String.fromCharCode(mapStart + bytes.length); encrypted += lengthMark; for (let i = 0; i < bytes.length; i += 2) { const byte1 = bytes[i]; const byte2 = i + 1 < bytes.length ? bytes[i + 1] : 0; const merged = (byte1 << 8) | byte2; encrypted += String.fromCharCode(mapStart + 256 + merged); } return encrypted; } compressDecrypt(encrypted) { if (encrypted.length <= 1) return "加密数据不完整"; const mapStart = this.CHINESE_RANGE.start; const bytesLength = encrypted.charCodeAt(0) - mapStart; const bytes = new Uint8Array(bytesLength); for (let i = 1, byteIndex = 0; i < encrypted.length; i++) { const merged = encrypted.charCodeAt(i) - mapStart - 256; if (byteIndex < bytesLength) { bytes[byteIndex++] = (merged >> 8) & 0xFF; } if (byteIndex < bytesLength) { bytes[byteIndex++] = merged & 0xFF; } } return new TextDecoder().decode(bytes); } // 工具方法 generateElegantColor(str) { let hash = 0; for (let i = 0; i < str.length; i++) { hash = str.charCodeAt(i) + ((hash << 5) - hash); } const hue = Math.abs(hash % 360); const saturation = 60 + Math.abs(hash % 30); const lightness = 40 + Math.abs(hash % 30); return `hsl(${hue}, ${saturation}%, ${lightness}%)`; } getContrastColor(hsl) { const lightness = parseInt(hsl.match(/hsl\((\d+),\s*(\d+)%,\s*(\d+)%\)/)[3]); return lightness > 60 ? 'rgba(0, 0, 0, 0.8)' : 'rgba(255, 255, 255, 0.9)'; } // 防抖函数 debounce(func, wait) { let timeout; return (...args) => { clearTimeout(timeout); timeout = setTimeout(() => func.apply(this, args), wait); }; } // 初始化方法 init() { // 防止重复注入 if (window._hasCtrmInjected) { return document.querySelector('.chat-title').click(); } window._hasCtrmInjected = true; this.injectStyles(); this.injectHTML(); // 添加延时确保 DOM 已经渲染 setTimeout(() => { this.cacheDOMElements(); this.bindEvents(); this.initAllConnections(); this.adjustUI(); // 初始化时折叠用户面板 this.elements.onlineUsersPanel.addClass('collapsed'); // 默认收起状态 setTimeout(() => { this.elements.chatContainer.show(); this.elements.closeBtn.click(); }, 100); }, 0); } // 注入样式 injectStyles() { const styles = ` /*-------------------- CSS变量定义 --------------------*/ :root { --primary-color: rgba(222, 184, 135, 0.8); /* 主色调 burlywood */ --primary-hover: rgba(222, 184, 135, 0.5); /* 悬停色 */ --bg-dark: rgba(0, 0, 0, 0.8); /* 深色背景 */ --bg-darker: rgba(0, 0, 0, 0.2); /* 更深色背景 */ --bg-lighter: rgba(135, 135, 135, 0.3); /* 较浅色背景 */ --text-primary: rgba(255, 255, 255, 0.9); /* 主要文字色 */ --text-secondary: rgba(255, 255, 255, 0.7); /* 次要文字色 */ --text-muted: rgba(255, 255, 255, 0.3); /* 弱化文字色 */ --border-color: rgba(255, 255, 255, 0.1); /* 边框色 */ --shadow-color: rgba(222, 184, 135, 0.5); /* 阴影色 */ --system-msg-bg: rgba(255, 152, 0, 0.5); /* 系统消息背景 */ --message-background-color: rgba(255, 255, 255, 0.95); --primary-text-color: rgba(0, 0, 0, 0.9); --peer-color-rgb: 0, 150, 135; } /* 原来的所有样式 */ ${this.getChatStyles()} `; const styleElement = document.createElement('style'); styleElement.textContent = styles; document.head.appendChild(styleElement); } // 注入HTML injectHTML() { const chatTemplate = ` <div id="ctrm_" style="display: none;"> <div class="chat"> <div class="chat-title"> <div class="chat-tabs"> <!-- 这里将动态添加标签 --> </div> <div class="chat-controls"> <button class="chat-reconn" title="重生"> <svg fill="currentColor" viewBox="0 0 8 8" width="16" height="16" xmlns="http://www.w3.org/2000/svg"> <path d="M4 0c-1.65 0-3 1.35-3 3h-1l1.5 2 1.5-2h-1c0-1.11.89-2 2-2v-1zm2.5 1l-1.5 2h1c0 1.11-.89 2-2 2v1c1.65 0 3-1.35 3-3h1l-1.5-2z" transform="translate(0 1)" /> </svg> </button> <button class="chat-close" title="老板出没"></button> </div> </div> <div class="messages"> <div class="messages-content"></div> <div class="scroll-bottom">⇩</div> <div class="online-users"> <div class="online-users-header">在线人数:0</div> <div class="online-users-content"></div> <div class="toggle-users-panel"></div> </div> </div> <div class="message-box"> <textarea type="text" class="message-input" placeholder="说点什么吧..." maxlength="69"></textarea> <button type="submit" class="message-submit">发送</button> </div> </div> </div> `; const container = document.createElement('div'); container.innerHTML = chatTemplate; document.body.appendChild(container.firstElementChild); } // 缓存DOM元素 cacheDOMElements() { const elements = { chatContainer: $('#ctrm_'), chatTitle: $('#ctrm_ .chat-title'), chatTabs: $('#ctrm_ .chat-tabs'), chatMessagesContent: $('#ctrm_ .messages-content'), scrollBottomBtn: $('#ctrm_ .scroll-bottom'), messageInput: $('#ctrm_ .message-input'), messag###bmitBtn: $('#ctrm_ .message-submit'), onlineUsersHeader: $('#ctrm_ .online-users-header'), onlineUsersContent: $('#ctrm_ .online-users-content'), toggleUsersPanelBtn: $('#ctrm_ .toggle-users-panel'), onlineUsersPanel: $('#ctrm_ .online-users'), closeBtn: $('#ctrm_ .chat-close'), reconnectBtn: $('#ctrm_ .chat-reconn') }; // 检查所有必需的元素是否存在 for (const [key, element] of Object.entries(elements)) { if (!element.length) { console.error(`Required element not found: ${key}`); throw new Error(`Required element not found: ${key}`); } } this.elements = elements; } // 绑定事件 bindEvents() { // 点击空白处关闭聊天框 $(document.body).on('click', this.handleOutsideClick.bind(this)); window.addEventListener('popstate', this.handleOutsideClick.bind(this)); // 阻止事件冒泡 this.elements.chatContainer.on('click', e => e.stopPropagation()); this.elements.chatContainer.on('touchstart', e => e.stopPropagation()); this.elements.chatContainer.on('touchend', e => e.stopPropagation()); this.elements.chatContainer.on('touchmove', e => e.stopPropagation()); // 聊天面板点击事件 this.elements.chatContainer.find('.chat').on('click', e => { if (this.elements.chatContainer.hasClass('ctrm-close')) { this.elements.chatContainer.removeClass('ctrm-close'); this.adjustUI(); // 面板展开时,确保滚动到最新消息 this.autoScroll = true; requestAnimationFrame(() => { this.scrollToBottom(); }); e.stopPropagation(); } }); // 关闭按钮事件 this.elements.closeBtn.click(e => { this.elements.chatContainer.toggleClass('ctrm-close'); this.adjustUI(); e.stopPropagation(); }); // 重连按钮事件 this.elements.reconnectBtn.click(() => this.reconnect()); // 发送按钮事件 this.elements.messag###bmitBtn.click(() => this.sendMessage()); // 输入框事件 this.elements.messageInput.on('keydown', e => { if (e.keyCode === 13 && !e.shiftKey) { e.preventDefault(); this.sendMessage(); } }); // 输入框自动调整高度 this.elements.messageInput.on('input', function() { this.style.height = '36px'; const newHeight = Math.min(this.scrollHeight, 120); this.style.height = newHeight + 'px'; }); // 滚动事件 this.elements.chatMessagesContent.on('scroll', this.debounce(() => this.handleScroll(), 100) ); // 滚动到底部按钮事件 this.elements.scrollBottomBtn.click(() => { this.elements.scrollBottomBtn.hide(); this.autoScroll = true; this.scrollToBottom(); }); // 用户面板切换按钮事件 this.elements.toggleUsersPanelBtn.click(() => { this.elements.onlineUsersPanel.toggleClass('collapsed'); // 切换消息区域的宽度 if (this.elements.onlineUsersPanel.hasClass('collapsed')) { this.elements.chatMessagesContent.addClass('full-width'); } else { this.elements.chatMessagesContent.removeClass('full-width'); } // 增加过渡效果后可能需要重新调整对话框 setTimeout(() => { this.scrollToBottom(); }, 300); }); // 移动端优化 this.setupMobileEvents(); } // 移动端事件优化 setupMobileEvents() { document.addEventListener('touchstart', e => { if($(e.target).closest('#ctrm_').length) { e.preventDefault(); } }, { passive: false }); this.elements.chatMessagesContent[0].addEventListener('scroll', e => { e.stopPropagation(); }, { passive: true }); this.elements.onlineUsersContent[0].addEventListener('scroll', e => { e.stopPropagation(); }, { passive: true }); } // 获取聊天室样式 getChatStyles() { return ` /*-------------------- CSS变量定义 --------------------*/ :root { --primary-color: rgba(222, 184, 135, 0.8); /* 主色调 burlywood */ --primary-hover: rgba(222, 184, 135, 0.5); /* 悬停色 */ --bg-dark: rgba(0, 0, 0, 0.8); /* 深色背景 */ --bg-darker: rgba(0, 0, 0, 0.2); /* 更深色背景 */ --bg-lighter: rgba(135, 135, 135, 0.3); /* 较浅色背景 */ --text-primary: rgba(255, 255, 255, 0.9); /* 主要文字色 */ --text-secondary: rgba(255, 255, 255, 0.7); /* 次要文字色 */ --text-muted: rgba(255, 255, 255, 0.3); /* 弱化文字色 */ --border-color: rgba(255, 255, 255, 0.1); /* 边框色 */ --shadow-color: rgba(222, 184, 135, 0.5); /* 阴影色 */ --system-msg-bg: rgba(255, 152, 0, 0.5); /* 系统消息背景 */ --message-background-color: rgba(255, 255, 255, 0.95); --primary-text-color: rgba(0, 0, 0, 0.9); --peer-color-rgb: 0, 150, 135; } /*-------------------- 基础样式 --------------------*/ #ctrm_ { position: fixed; z-index: 10001; bottom: 0; right: 0; transition: all 0.3s ease; font-family: 'Open Sans', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; font-size: 12px; line-height: 1.3; height: 0; width: 0; } #ctrm_ .chat { position: fixed; bottom: 0; right: 0; width: 350px; height: 500px; z-index: 2; overflow: hidden; box-shadow: 0 0px 5px var(--shadow-color); background: var(--bg-lighter); backdrop-filter: blur(10px); border-radius: 20px 0 0 0; display: flex; flex-direction: column; transition: all 0.3s ease; border: 1px solid var(--border-color); --message-background-color: rgba(255, 255, 255, 0.95); --primary-text-color: rgba(0, 0, 0, 0.9); } #ctrm_ .chat-title { flex: 0 0 45px; position: relative; background: var(--bg-darker); color: var(--text-primary); display: flex; justify-content: space-between; align-items: center; padding: 0 10px; cursor: pointer; border-radius: 20px 0 0 0; z-index: 3; } #ctrm_ .chat-title.glow { background: color-mix(in srgb, var(--primary-color) 70%, transparent); } #ctrm_ .chat-tabs { display: flex; overflow-x: auto; white-space: nowrap; scrollbar-width: none; /* Firefox */ -ms-overflow-style: none; /* IE and Edge */ padding: 5px 0; max-width: calc(100% - 90px); /* 为右侧按钮留出空间 */ } #ctrm_ .chat-tabs::-webkit-scrollbar { display: none; /* Chrome, Safari and Opera */ } #ctrm_ .chat-tab { flex: 0 0 auto; /* 防止标签被压缩 */ padding: 6px 12px; margin-right: 6px; border-radius: 15px; background: var(--bg-lighter); cursor: pointer; white-space: nowrap; font-size: 12px; transition: all 0.2s ease; color: var(--text-secondary); display: inline-block; /* 确保标签内联显示 */ } #ctrm_ .chat-tab.active { background: var(--primary-color); color: var(--text-primary); } #ctrm_ .chat-tab .unread-indicator { display: inline-block; width: 8px; height: 8px; border-radius: 50%; background: #ff5252; margin-left: 4px; } #ctrm_.ctrm-mobile .chat-tab { padding: 6px 14px; font-size: 14px; } /*-------------------- 控制按钮 --------------------*/ #ctrm_ .chat-controls { display: flex; align-items: center; } #ctrm_ .chat-reconn, #ctrm_ .chat-close { display: flex; align-items: center; justify-content: center; width: 30px; height: 30px; font-size: 14px; border-radius: 50%; cursor: pointer; background: var(--bg-lighter); color: var(--text-secondary); margin-left: 6px; border: none; transition: all 0.2s ease; } #ctrm_ .chat-reconn svg { width: 16px !important; /* 增加优先级 */ height: 16px !important; /* 增加优先级 */ min-width: 16px !important; /* 确保最小尺寸 */ min-height: 16px !important; /* 确保最小尺寸 */ transition: transform 0.3s ease; } #ctrm_ .chat-reconn:hover, #ctrm_ .chat-close:hover { background: var(--bg-darker); color: var(--text-primary); } /*-------------------- 消息区域 --------------------*/ #ctrm_ .messages { flex: 1; position: relative; color: var(--text-secondary); overflow: hidden; } #ctrm_ .messages-content { position: absolute; top: 0; left: 0; height: 100%; width: calc(100% - 130px); /* 为右侧用户列表留出空间 */ overflow-y: auto; padding: 10px 15px; scrollbar-width: none; /* Firefox */ overscroll-behavior: contain; /* 阻止滚动链 */ touch-action: pan-y; /* 仅允许垂直滚动 */ } #ctrm_ .messages-content::-webkit-scrollbar { display: none; /* Chrome/Safari */ } /*-------------------- 消息气泡 --------------------*/ #ctrm_ .message { margin: 0; clear: none; float: none; display: inline-block; padding: 6px 10px 7px; border-radius: 10px 10px 10px 0; background: rgba(0, 0, 0, 0.3); font-size: 12px; line-height: 1.4; position: relative; box-shadow: 0 1px 2px rgba(16, 35, 47, 0.15); max-width: 85%; min-width: 50px; word-wrap: break-word; animation: fadeIn 0.2s ease; border: none; color: rgba(255, 255, 255, 0.9); } #ctrm_ .message .timestamp { position: absolute; right: 5px; bottom: 2px; font-size: 9px; color: rgba(255, 255, 255, 0.5); } #ctrm_ .message .username { display: block; font-weight: 600; color: var(--bg-color) !important; text-shadow: 0 1px 1px rgba(0, 0, 0, 0.2); } #ctrm_ .message::before { content: ''; position: absolute; left: -11px; bottom: 0; width: 11px; height: 20px; background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 11 20' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M0 20h11V0C11 5 8 10 0 20z' fill='rgba(0, 0, 0, 0.3)'/%3E%3C/svg%3E"); background-size: contain; background-repeat: no-repeat; } #ctrm_ .message.message-personal::before { left: auto; right: -11px; transform: scaleX(-1); background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 11 20' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M0 20h11V0C11 5 8 10 0 20z' fill='%23806e58'/%3E%3C/svg%3E"); /* 使用主题色 */ } /*-------------------- 头像样式 --------------------*/ #ctrm_ .messages .avatar { position: absolute; z-index: 1; left: -6px; // 不要修改 bottom: 0; transform: none; border-radius: 30px; width: 30px; height: 30px; margin: 0; padding: 0; border: none; box-shadow: 0 1px 2px rgba(16, 35, 47, 0.15); display: flex; align-items: center; justify-content: center; background: var(--bg-color); color: var(--text-color); font-size: 14px; font-weight: bold; text-shadow: 0 1px 1px rgba(0, 0, 0, 0.1); } #ctrm_ .messages .avatar span { color: var(--text-primary); text-shadow: 0 1px 2px var(--shadow-color); font-size: 16px; -webkit-font-smoothing: antialiased; text-rendering: optimizeLegibility; } #ctrm_ .message.message-personal { margin-left: auto; margin-right: 0; color: rgba(255, 255, 255, 0.9); text-align: left; background: linear-gradient(120deg, color-mix(in srgb, var(--primary-color) 90%, transparent), color-mix(in srgb, var(--primary-hover) 90%, transparent) ); border-radius: 10px 10px 0 10px; border: none; } #ctrm_ .message.system-message { background: var(--system-msg-bg); text-align: center; float: none; margin: 8px auto; clear: both; color: var(--text-primary); width: auto; display: inline-block; border-radius: 10px; // 四角统一圆角 padding: 6px 15px; // 增加内边距 } #ctrm_ .message.system-message::before { display: none; } /*-------------------- 滚动到底部按钮 --------------------*/ #ctrm_ .scroll-bottom { position: absolute; bottom: 20px; right: 20px; width: 36px; height: 36px; background: color-mix(in srgb, var(--primary-color) 80%, transparent); border-radius: 50%; display: flex; align-items: center; justify-content: center; color: var(--text-primary); font-size: 16px; cursor: pointer; box-shadow: 0 2px 5px color-mix(in srgb, var(--shadow-color) 20%, transparent); transition: all 0.2s ease; z-index: 2; display: none; text-align: center; line-height: 36px; } #ctrm_ .scroll-bottom:hover { background: var(--primary-color); } /*-------------------- 输入框区域 --------------------*/ #ctrm_ .message-box { flex: 0 0 auto; /* 改为固定高度 */ padding: 8px 10px; position: relative; background: var(--bg-darker); min-height: 52px; /* 设置最小高度 = padding + input最小高度 */ } #ctrm_ .message-input { box-sizing: border-box; min-height: 36px; /* 设置输入框最小高度 */ max-height: 120px; /* 设置最大高度限制 */ height: 36px; /* 默认高度等于最小高度 */ padding: 8px 10px; line-height: 20px; /* 设置行高 */ width: calc(100% - 64px); /* 为发送按钮留出空间 */ border-radius: 18px; resize: none; background: var(--bg-darker); border: none; outline: none; color: var(--text-primary); overflow-y: auto; /* 允许垂直滚动 */ transition: height 0.1s ease; /* 添加高度变化动画 */ } #ctrm_ .message-input::placeholder { color: var(--text-muted); } /* 隐藏所有滚动条但保留滚动功能 */ #ctrm_ .message-input::-webkit-scrollbar { display: none; /* Chrome/Safari */ } #ctrm_ .message-input { scrollbar-width: none; /* Firefox */ } #ctrm_ .message-submit { top: 50%; transform: translateY(-50%); right: 10px; margin: 0; position: absolute; color: var(--text-primary); border: none; background: var(--primary-color); font-size: 12px; text-transform: uppercase; line-height: 1; padding: 8px 15px; border-radius: 15px; outline: none !important; transition: background .2s ease; cursor: pointer; box-shadow: 0 2px 5px color-mix(in srgb, var(--shadow-color) 30%, transparent); } #ctrm_ .message-submit:hover { background: var(--primary-hover); } /*-------------------- 在线用户面板 --------------------*/ #ctrm_ .online-users { position: absolute; right: 0; top: 0; width: 130px; height: 100%; background: var(--bg-lighter); transition: transform 0.3s ease; z-index: 2; border-left: 1px solid var(--border-color); } #ctrm_ .online-users-header { text-align: center; padding: 5px 5px; font-size: 12px; color: var(--text-secondary); border-bottom: 1px solid var(--border-color); } #ctrm_ .online-users.collapsed { transform: translateX(130px); } #ctrm_ .online-users-content { height: calc(100% - 34px); overflow-y: auto; padding: 5px 5px; scrollbar-width: none; /* Firefox */ overscroll-behavior: contain; touch-action: pan-y; } #ctrm_ .messages-content.full-width { width: 100%; /* 当用户面板折叠时使用全宽 */ } #ctrm_ .online-user { padding: 6px 10px; border-radius: 15px; margin: 4px 0; font-size: 11px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; cursor: pointer; transition: all 0.2s ease; color: var(--text-secondary); background: var(--bg-lighter); text-align: center; } #ctrm_ .online-user:hover { transform: translateX(-2px); } #ctrm_ .online-user.self { background: var(--primary-color); color: var(--text-primary); } #ctrm_ .toggle-users-panel { position: absolute; left: -11px; top: 50%; transform: translateY(-50%); width: 10px; height: 50px; background: var(--bg-darker); border-radius: 4px 0 0 4px; display: flex; align-items: center; justify-content: center; cursor: pointer; font-size: 12px; color: var(--text-secondary); } #ctrm_ .toggle-users-panel::after { content: ">"; } #ctrm_ .online-users.collapsed .toggle-users-panel::after { content: "<"; } #ctrm_ .toggle-users-panel:hover { color: var(--text-primary); } /*-------------------- 消息容器 --------------------*/ #ctrm_ .message-container { position: relative; min-height: 40px; margin: 16px 0 20px; clear: both; padding-left: 35px; display: flex; align-items: flex-end; width: 100%; } #ctrm_ .message-container .message { margin-bottom: 0; } #ctrm_.ctrm-close .chat-close::after { content: "▲"; } #ctrm_ .chat-close::after { content: "▼"; } #ctrm_.ctrm-close .toggle-users-panel { display: none; } #ctrm_ .message-text { display: block; margin-top: 4px; padding-bottom: 2px; } #ctrm_ .message-container:has(.message-personal) { justify-content: flex-end; padding-left: 0; padding-right: 35px; } #ctrm_ .message-container:has(.message-personal) .avatar { left: auto; right: -6px; bottom: 0; } /* 修改收起状态样式 */ #ctrm_.ctrm-close .chat { height: 45px !important; /* 仅显示标题高度 */ width: auto !important; min-width: 120px; } #ctrm_.ctrm-close .chat-tabs { max-width: 200px; overflow: hidden; } #ctrm_.ctrm-close .messages, #ctrm_.ctrm-close .message-box, #ctrm_.ctrm-close .online-users { display: none !important; } @keyframes tab-pulse { 0% { box-shadow: 0 0 0 0 rgba(255,82,82,0.4); } 70% { box-shadow: 0 0 0 6px rgba(255,82,82,0); } 100% { box-shadow: 0 0 0 0 rgba(255,82,82,0); } } #ctrm_ .chat-tab.unread-pulse { animation: tab-pulse 1.5s infinite; position: relative; } @keyframes fadeIn { 0% { opacity: 0; transform: translateY(10px); } 100% { opacity: 1; transform: translateY(0); } } `; } // WebSocket 相关方法 initAllConnections() { const domainInfo = this.getDomainInfo(); let connectionsInitiated = 0; let connectionsSucceeded = 0; // 计算需要建立的连接数 const totalConnections = 1 + 1 + (domainInfo.isSpecialSite && domainInfo.specialCode ? 1 : 0); const checkAllConnections = () => { if (connectionsSucceeded === totalConnections) { console.log("所有聊天室连接成功!"); if (this.isReconnecting) { this.appendSystemMessage("所有聊天室已重新连接成功!"); } } else if (connectionsInitiated === totalConnections && connectionsSucceeded < totalConnections) { console.log(`部分聊天室连接失败,成功连接 ${connectionsSucceeded}/${totalConnections} 个聊天室`); if (this.isReconnecting) { this.appendSystemMessage(`部分聊天室连接失败,成功连接 ${connectionsSucceeded}/${totalConnections} 个聊天室`); } } }; // 连接特殊房间(如果有) if (domainInfo.isSpecialSite && domainInfo.specialCode) { connectionsInitiated++; this.initDomainConnection(domainInfo.specialCode, () => { connectionsSucceeded++; checkAllConnections(); }); } // 连接当前站点 connectionsInitiated++; this.initDomainConnection(domainInfo.currentHostname, () => { connectionsSucceeded++; checkAllConnections(); }); // 连接大厅 connectionsInitiated++; this.initDomainConnection(this.HALL_DOMAIN, () => { connectionsSucceeded++; checkAllConnections(); }); } initDomainConnection(domain, onSuccess) { if (!this.domainData[domain]) { this.domainData[domain] = { messages: [], users: [], unreadCount: 0, connected: false, userId: null, userName: null }; } if (!this.activeWebsockets[domain]) { const ws = new WebSocket(this.config.wsServer); ws.onopen = () => { const updateMsg = { type: 'update', data: { domainFrom: domain }, char: this.authChar }; ws.send(JSON.stringify(updateMsg)); this.domainData[domain].connected = true; if (!this.activeDomain) { this.activeDomain = domain; this.updateUI(); } this.updateTabs(); if (onSuccess) onSuccess(); }; ws.onmessage = (event) => this.handleDomainMessage(domain, event); ws.onclose = () => this.handleDomainDisconnect(domain); ws.onerror = (error) => { console.error(`WebSocket Error (${domain}):`, error); this.handleDomainDisconnect(domain); }; this.activeWebsockets[domain] = ws; this.addDomainTab(domain); } } handleDomainMessage(domain, event) { const message = JSON.parse(event.data); const type = message.type; const data = message.data; switch (type) { case 'identity': this.domainData[domain].userId = data.id; this.domainData[domain].userName = data.name; if (data.history && data.history.length > 0) { this.domainData[domain].messages = data.history.map(msg => ({ ...msg, msg: this.decrypt(msg.msg) })); if (domain === this.activeDomain) { this.updateUI(); // 确保历史记录加载后滚动到底部 this.autoScroll = true; // 使用requestAnimationFrame确保DOM渲染完成后再滚动 requestAnimationFrame(() => { this.scrollToBottom(); }); } } break; case 'memberList': this.domainData[domain].users = data.filter(user => user.id !== "12523461428" && user.name !== "小尬" ); // 无论当前活跃域名是什么,都更新标签显示 this.updateTabs(); if (domain === this.activeDomain) { this.updateOnlineUsers(); } break; case 'chat': data.msg = this.decrypt(data.msg); this.domainData[domain].messages.push(data); if (domain === this.activeDomain) { this.appendMessage(data); if (this.autoScroll) { this.scrollToBottom(); } } else { this.domainData[domain].unreadCount++; this.updateTabs(); } if (this.elements.chatContainer.hasClass('ctrm-close') && domain === this.activeDomain) { this.domainData[domain].unreadCount++; this.updateTabs(); } break; case 'ack': if (domain === this.activeDomain) { this.elements.messageInput.val(''); } break; } this.trimHistory(); } handleDomainDisconnect(domain) { this.domainData[domain].connected = false; this.domainData[domain].users = []; if (domain === this.activeDomain) { this.appendSystemMessage("您已掉线,点击重生按钮重新连接..."); this.updateOnlineUsers(); } this.updateTabs(); } // 消息相关方法 sendMessage() { const message = this.sanitizeMessage( this.elements.messageInput.val().slice(0, 69).trim() ); if (message.length === 0) { return alert('消息不能为空'); } if (!this.activeWebsockets[this.activeDomain] || !this.domainData[this.activeDomain].connected) { return alert('当前聊天室未连接,请重新连接'); } if (!this.isSending) { const chatMessage = { type: 'chat', data: { msg: this.encrypt(message) }, char: this.authChar }; try { this.isSending = true; this.activeWebsockets[this.activeDomain].send(JSON.stringify(chatMessage)); setTimeout(() => { this.isSending = false; }, 5000); } catch (error) { console.error('发送消息失败:', error); alert('发送消息失败,请检查网络连接'); this.isSending = false; } } } // UI 更新相关方法 updateUI() { try { this.elements.chatMessagesContent.empty(); const messages = this.domainData[this.activeDomain]?.messages || []; messages.forEach(msg => { const element = this.createMessageElement(msg); if (element) { this.elements.chatMessagesContent.append(element); } }); // 历史记录加载时,总是滚动到底部 this.autoScroll = true; // 使用requestAnimationFrame确保DOM渲染完成后再滚动 requestAnimationFrame(() => { this.scrollToBottom(); }); this.updateOnlineUsers(); this.updateTabs(); } catch (error) { console.error('Error updating UI:', error); this.appendSystemMessage("更新界面时出现错误"); } } createMessageElement(msg) { const time = new Date(msg.time); const timeStr = `${time.getHours().toString().padStart(2, '0')}:${time.getMinutes().toString().padStart(2, '0')}`; const isMe = msg.id === this.domainData[this.activeDomain].userId; const isSystem = msg.id === "system"; let $messageElement; if (isSystem) { $messageElement = $(`<div class="message system-message"></div>`).text(msg.msg); } else { const firstChar = msg.name.charAt(0); const bgColor = this.generateElegantColor(msg.name); const textColor = this.getContrastColor(bgColor); if (!isMe) { $messageElement = $(` <div class="message-container"> <figure class="avatar" style="--bg-color:${bgColor}; --text-color:${textColor}"> <span>${firstChar}</span> </figure> <div class="message" style="--bg-color:${bgColor}"> <div class="username">${msg.name}</div> <span class="message-text">${msg.msg}</span> <div class="timestamp">${timeStr}</div> </div> </div> `); } else { $messageElement = $(` <div class="message-container"> <div class="message message-personal" style="--bg-color:${bgColor}"> <div class="username">${msg.name}</div> <span class="message-text">${msg.msg}</span> <div class="timestamp">${timeStr}</div> </div> <figure class="avatar" style="--bg-color:${bgColor}; --text-color:${textColor}"> <span>${firstChar}</span> </figure> </div> `); } } return $messageElement[0]; } // 域名检测和特殊站点处理 getDomainInfo() { const currentUrl = window.location.href; const currentHostname = location.hostname; const r###lt = { currentHostname: currentHostname, specialCode: null, isSpecialSite: false }; if (currentUrl.includes('missav')) { r###lt.isSpecialSite = true; const code = this.extractMissavCode(currentUrl); if (code) { r###lt.specialCode = code + '.av'; } } else if (currentUrl.includes('jable')) { r###lt.isSpecialSite = true; const code = this.extractJableCode(currentUrl); if (code) { r###lt.specialCode = code + '.av'; } } return r###lt; } extractMissavCode(currentUrl) { if (/https?:\/\/(www\.)?missav\.(com|ai|ws|net)/i.test(currentUrl)) { const urlParts = currentUrl.split('/'); const lastPart = urlParts[urlParts.length - 1]; if (/^\d+$/.test(lastPart)) { return null; } if (lastPart.includes('-')) { const segments = lastPart.split('-'); if (lastPart.toLowerCase().startsWith('fc2-ppv')) { return `fc2-ppv-${segments[2]}`; } if (lastPart.toLowerCase().startsWith('caribbeancom')) { return segments.slice(0, 3).join('-'); } if (/^[a-zA-Z]{2,5}-\d{3,6}/.test(lastPart)) { return segments.slice(0, 2).join('-'); } return segments.slice(0, 2).join('-'); } else { if (/^[a-zA-Z]+\d+$/.test(lastPart)) { return lastPart; } return null; } } return null; } extractJableCode(currentUrl) { if (/https?:\/\/(www\.)?jable\.tv/i.test(currentUrl)) { const urlParts = currentUrl.split('/'); let videoId = null; for (let i = 0; i < urlParts.length; i++) { if (urlParts[i] === 'videos' && i + 1 < urlParts.length) { videoId = urlParts[i + 1].replace(/\/$/, ''); break; } } if (!videoId) return null; if (/^\d{6}-\d{3}$/.test(videoId)) { return `caribbeancom-${videoId}`; } if (/^fc2ppv-\d+/.test(videoId.toLowerCase())) { const fc2Num = videoId.split('-')[1]; return `fc2-ppv-${fc2Num}`; } if (/^[a-zA-Z]+-\d+(-[a-zA-Z])?$/.test(videoId)) { const parts = videoId.split('-'); if (parts.length > 2 && parts[2].length <= 2) { return `${parts[0]}-${parts[1]}`; } } if (/^[a-zA-Z]+-\d+$/.test(videoId)) { return videoId; } return videoId; } return null; } // 标签管理相关方法 addDomainTab(domain) { if (this.elements.chatTabs.find(`.chat-tab[data-domain="${domain}"]`).length === 0) { let displayName = domain; if (domain === this.HALL_DOMAIN) { displayName = "大厅"; } else if (domain === location.hostname) { displayName = "当前站点"; } else if (domain.endsWith('.av')) { displayName = domain.replace('.av', ''); } const tab = $(` <div class="chat-tab" data-domain="${domain}"> ${displayName} <span class="user-count">(0)</span> </div> `); tab.on('click', () => this.switchDomain(domain)); this.elements.chatTabs.append(tab); if (this.elements.chatTabs.find('.chat-tab').length === 1) { tab.addClass('active'); } } } updateTabs() { this.elements.chatTabs.find('.chat-tab').each((_, tab) => { const $tab = $(tab); const domain = $tab.data('domain'); const domainInfo = this.domainData[domain]; $tab.removeClass('active disconnected unread-pulse'); $tab.find('.unread-indicator').remove(); // 更新在线人数 const userCount = domainInfo.users ? domainInfo.users.length : 0; $tab.find('.user-count').text(`(${userCount})`); if (domain === this.activeDomain) { $tab.addClass('active'); if (!this.elements.chatContainer.hasClass('ctrm-close')) { domainInfo.unreadCount = 0; } } if (!domainInfo.connected) { $tab.addClass('disconnected'); } if (domainInfo.unreadCount > 0 && (this.elements.chatContainer.hasClass('ctrm-close') || domain !== this.activeDomain)) { $tab.append(`<span class="unread-indicator"></span>`); $tab.addClass('unread-pulse'); } }); } // 域名切换相关方法 switchDomain(domain) { if (domain === this.activeDomain) return; const previousTab = this.elements.chatTabs.find(`.chat-tab[data-domain="${this.activeDomain}"]`); previousTab.removeClass('unread-pulse'); this.activeDomain = domain; this.domainData[domain].unreadCount = 0; this.elements.chatMessagesContent.empty(); if (this.domainData[domain] && this.domainData[domain].messages) { this.domainData[domain].messages.forEach(msg => { const element = this.createMessageElement(msg); if (element) { this.elements.chatMessagesContent.append(element); } }); } this.updateOnlineUsers(); this.updateTabs(); // 切换域名时,确保滚动到底部 this.autoScroll = true; // 使用requestAnimationFrame确保DOM渲染完成后再滚动 requestAnimationFrame(() => { this.scrollToBottom(); }); } // 重连相关方法 reconnect() { if (!this.isReconnecting) { this.isReconnecting = true; Object.keys(this.activeWebsockets).forEach(domain => { if (this.activeWebsockets[domain]) { this.activeWebsockets[domain].close(); this.activeWebsockets[domain] = null; } this.domainData[domain].messages = []; this.domainData[domain].users = []; this.domainData[domain].connected = false; }); this.elements.chatMessagesContent.empty(); this.elements.messageInput.val(''); this.appendSystemMessage("正在重新连接所有聊天室..."); this.initAllConnections(); setTimeout(() => { this.isReconnecting = false; }, 2000); } } // 消息历史记录管理 trimHistory() { if (this.domainData[this.activeDomain].messages.length > this.config.maxHistory) { this.domainData[this.activeDomain].messages = this.domainData[this.activeDomain].messages.slice(-this.config.maxHistory); } } // 在线用户管理 updateOnlineUsers() { this.elements.onlineUsersContent.empty(); const currentUsers = this.domainData[this.activeDomain]?.users || []; const currentUserId = this.domainData[this.activeDomain]?.userId; // 更新在线人数显示 this.elements.onlineUsersHeader.text(`在线人数:${currentUsers.length}`); currentUsers.forEach(user => { const isCurrentUser = user.id === currentUserId; const userClass = isCurrentUser ? 'online-user self' : 'online-user'; const userElement = $(` <div class="${userClass}" data-id="${user.id}"> ${user.name} </div> `); this.elements.onlineUsersContent.append(userElement); }); } // 滚动处理相关方法 handleScroll() { const el = this.elements.chatMessagesContent[0]; const clientHeight = el.clientHeight; const scrollTop = el.scrollTop; this.autoScroll = clientHeight + scrollTop >= el.scrollHeight * 0.9; this.elements.scrollBottomBtn.toggle(!this.autoScroll); } scrollToBottom() { const el = this.elements.chatMessagesContent[0]; el.scrollTop = el.scrollHeight; } // 辅助方法 sanitizeMessage(message) { return message.replace(/</g, '<').replace(/>/g, '>'); } appendSystemMessage(message) { const systemMsg = { time: Date.now(), id: "system", name: "系统消息", msg: message }; this.appendMessage(systemMsg); } appendMessage(data, scroll = true) { try { const element = this.createMessageElement(data); if (element) { this.elements.chatMessagesContent.append(element); if (scroll && this.autoScroll) { requestAnimationFrame(() => this.scrollToBottom()); } } } catch (error) { console.error('Error appending message:', error); } } handleOutsideClick(event) { if (!this.elements.chatContainer.is(event.target) && this.elements.chatContainer.has(event.target).length === 0 && !this.elements.chatContainer.hasClass('ctrm-close')) { this.elements.chatContainer.addClass('ctrm-close'); } } adjustUI() { if (window.innerWidth < 768) { this.elements.chatContainer.addClass('ctrm-mobile'); } else { this.elements.chatContainer.removeClass('ctrm-mobile'); } } } // 导出类 if (typeof module !== 'undefined' && module.exports) { module.exports = ChatRoom; } else if (typeof window !== 'undefined') { window.ChatRoom = ChatRoom; }