🏠 Home 

Chat_Room

聊天室插件

Этот скрипт недоступен для установки пользователем. Он является библиотекой, которая подключается к другим скриптам мета-ключом // @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, '&lt;').replace(/>/g, '&gt;');
}
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;
}