xiuno 的内置聊天室(粗暴版本/非插件版本)

前两天用 AI 搞得聊天室,陆陆续续优化了两天,懒得后续优化了,打包发上来。

注:非插件应用,直接把代码放到网站即可使用。

懒得弄成插件,起初是用 json 存储数据的,但会遇到读取和写入同时进行时,偶发性让记录清空,尝试加上文件锁,但还是有清空情况。

于是现在的版本是改成了数据库存储的,但独立于论坛的数据库。

实现的原理很简单,获取本人已登录的个人信息,发消息时存入数据库即可(所以说其它程序也能使用,只要改一下获取用户信息的部分即可。甚至可以完全去掉改成博客那种留邮箱发消息的效果)。

预览:

图片[1]|xiuno 的内置聊天室(粗暴版本/非插件版本)|不死鸟资源网

PS:如有需要,请自行修改文件目录,代码很垃,请勿点评。

html 部分:

<?php if (!empty($uid)): ?>
    <style>
        .chat-container {
            position: fixed;
            bottom: 30px;
            right: 30px;
            z-index: 999;
            transition: all 0.3s ease;
        }

        .chat-box {
    width: 340px;
    background-color: white;
    border-radius: 8px;
    box-shadow: 0 0 10px rgb(0 0 0 / 73%);
    overflow: hidden;
    display: none;
    position: relative;
    animation: fadeIn 0.3s ease;
}

        @keyframes fadeIn {
            from {
                opacity: 0;
                transform: scale(0.9);
            }

            to {
                opacity: 1;
                transform: scale(1);
            }
        }

        @keyframes fadeOut {
            from {
                opacity: 1;
                transform: scale(1);
            }

            to {
                opacity: 0;
                transform: scale(0.9);
            }
        }

        .chat-header {
            display: flex;
            justify-content: space-between;
            align-items: center;
            padding: 10px 15px;
            background-color: #f7f6f1;
            border-bottom: 1px solid #e2e8f0;
        }

        .chat-header h3 {
            margin: 0;
            font-size: 16px;
            color: #333;
        }

        .chat-header button {
            background: none;
            border: none;
            cursor: pointer;
            font-size: 18px;
            color: #333;
            outline: none;
            /* 取消按钮点击时的边框 */
        }

        div#chat-box {
            position: relative;
            overflow: hidden;
            background-color: #f7f6f1;
            padding: 2px 15px;
            overflow-y: scroll;
            height: 30rem;
        }

        .flex.items-start {
            align-items: flex-start;
            display: flex;
        }

        .max-w-\[75\%\].flex.flex-col.items-start {
            --tw-space-x-reverse: 0;
            margin-right: calc(.5rem * var(--tw-space-x-reverse));
            margin-left: calc(.5rem * (1 - var(--tw-space-x-reverse)));
            align-items: flex-start;
            flex-direction: column;
            margin-left:.2rem;
        }

        .text-xs.text-blue-600.font-semibold.mb-1.no-underline {
            text-decoration-line: none;
            font-size: 0.75rem;
            line-height: 1rem;
            --tw-text-opacity: 1;
        }

        .bg-gray-200.mees {
            font-size:.875rem;
            line-height: 1.25rem;
            padding:.5rem;
            --tw-bg-opacity: 1;
            background-color: #dfdfdf;
            border-top-left-radius: 0;
            border-radius:.75rem;
            word-break: break-all;
            border-radius:.75rem;
            border-top-left-radius: 0;
            color: #6A4A3C;
            width: auto;
        }

        .flex {
            display: flex;
        }

        .input#message-input {
            flex: 1;
            min-width: 0;
            padding: 0.75rem 1rem;
            font-size: 0.875rem;
            border: none;
            /* 取消输入框边框 */
            border-right: none;
            border-top-left-radius: 0.5rem;
            border-bottom-left-radius: 2px;
            outline: none;
            /* 取消输入框聚焦时的边框 */
            padding-left: 10px;
        }

        .text-white {
            padding: 0.75rem 1rem;
            color: white;
            border: none;
            border-top-right-radius: 0.5rem;
            border-bottom-right-radius: 2px;
            transition: all 0.3s ease;
            flex-shrink: 0;
            width: 48px;
            display: flex;
            align-items: center;
            justify-content: center;
        }

        .chat-message-left {
            margin-right: auto;
        }

        .chat-message-right {
            margin-left: auto;
            flex-direction: row-reverse;
        }

        .chat-message-right.max-w-\[75\%\].flex.flex-col.items-start {
            align-items: flex-end;
        }

        .chat-message-right.bg-gray-200.mees {
            border-top-left-radius:.75rem;
            border-top-right-radius: 0;
        }

        .text-xs.text-gray-500 {
            font-size: 8px;
            color: #9ea4a9;
        }

        .beta-icon {
            color: #2e8786;
            font-size: 9px;
            font-weight: 400;
            position: relative;
            top: -11px;
            margin-left:.3rem;
        }

        .unread-indicator {
            position: absolute;
            top: 20%;
            left: 50%;
            transform: translate(-50%, -50%);
            background-color: #f00;
            color: white;
            border-radius: 2%;
            padding: 2px 10px;
            font-size: 10px;
            display: none;
        }

        .last-seen {
            text-align: center;
            color: #9ea4a9;
            font-size: 10px;
            margin-top: 5px;
            z-index: 30;
        }

        button#refresh-button {
            position: relative;
            background: none;
            border: none;
            cursor: pointer;
            font-size: 18px;
            color: #333;
            outline: none;
            /* 取消刷新按钮点击时的边框 */
            transition: transform 0.3s ease;
            /* 添加过渡效果 */
        }

        button#refresh-button:disabled {
            opacity: 0.6;
            cursor: not-allowed;
        }

        input#message-input {
            width: 100%;
            height: 40px;
            -webkit-appearance: none;
            -moz-appearance: none;
            appearance: none;
            box-shadow: none;
    padding-left: 10px;
        }

        .time>span {
            display: inline-block;
            padding: 0 15px;
            font-size: 9pt;
            color: #fff;
            border-radius: 2px;
            background-color: #dcdcdc;
        }

        p.time {
            margin: 9px 0;
            text-align: center;
        }

        /* 缩小图标样式 */
  
.chat-icon {
    position: fixed;
    bottom: 40px;
    right: 28px;
    background-color: #38b2ac;
    color: white;
    width: 50px;
    height: 50px;
    border-radius: 50%;
    display: flex
;
    justify-content: center;
    align-items: center;
    cursor: pointer;
    box-shadow: 0 0 10px rgba(0, 0, 0, 0.2);
    transition: all 0.3s ease;
}

        input {
            border: none;
            outline: none;
        }
        .input#message-input {
    /* 新增移动端适配 */
    height: 44px; /* 适配移动端输入框高度 */
    padding: 0.75rem 1rem;
}

button#send-button {
    /* 优化移动端点击区域 */
    width: 60px;
}
.border-red-500 {
    border: 2px solid #dc2626 !important; /* 红色边框 */
    color: #dc2626;
}

img.avatar-chat.w-8.h-8.rounded-full.mr-2 {
    border-radius: 1.3rem;
}
.fa-paper-plane:before {
    content: "\f1d8";
    font-size: 22px;
}
/* 新增声音按钮样式 */
.sound-button {
    background: none;
    border: none;
    cursor: pointer;
    font-size: 18px;
    color: #333;
    outline: none;
    transition: color 0.3s ease;
}

.sound-button.muted {
    color: #9ea4a9;
}
/* 灯箱样式 */
.lightbox {
    display: none; /* 默认隐藏 */
    position: absolute;
    top: 47px;
    left: 0;
    width: 100%;
    height: 83%;
    background: rgba(0, 0, 0, 0.8);
    z-index: 1000;
    justify-content: center;
    align-items: center;
    overflow: auto;
}

.lightbox-content {
    position: relative;
    max-width: 100%;
    text-align: center;
    height: auto;
}

#lightbox-image {
    max-width: 100%;
    max-height: 100vh;
    display: block;
    margin: 0 auto;
}

.close-lightbox {
    position: absolute;
    top: 10px;
    right: 10px;
    color: white;
    font-size: 24px;
    font-weight: bold;
    cursor: pointer;
}
.video-container {
    position: relative;
    display: inline-block;
}

.play-button {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    background: rgba(0, 0, 0, 0.5);
    cursor: pointer;
    z-index: 1;
    border-radius: .75rem;
    border-top-left-radius: 0;
}

.play-icon {
    font-size: 2.5rem;
    font-weight: bold;
}
.video-container video {
    border-radius: .75rem;
    border-top-left-radius: 0;
}
img.chat-image {
    border-radius: .75rem;
    border-top-left-radius: 0;
    display: block; 
}
button#send-button {
    background-image: linear-gradient(135deg, #38b2ac 0%, #2c7a7b 100%);
    color: white;
    border: none;
    border-top-right-radius: 0.5rem;
    border-bottom-right-radius: 0.5rem;
    transition: all 0.3s ease;
    flex-shrink: 0;
    width: 48px;
    display: flex
;
    align-items: center;
    justify-content: center;
    outline: none;
}

.video-container {
  display: block; 
}

.video-container video {
  vertical-align: top; 
  display: block; 
  margin: 0 auto; 
}


.flex.items-start.chat-self {
    justify-content: flex-end;
}

.chat-self .max-w-\[75\%\].flex.flex-col.items-start {
    align-items: flex-end;
}

.chat-self img.avatar-chat.w-8.h-8.rounded-full.mr-2 {
    order: 1;
    margin-right: 0 !important;
    margin-left: 0.5rem;
}

.chat-self .bg-gray-200.mees {
    border-top-left-radius: .75rem;
    border-top-right-radius: 0;
    background-color: #b2e281!important;
}
/* 自身消息(靠右)的图片样式 */
.chat-self .chat-image {
    border-top-right-radius: 0;
    border-top-left-radius: 0.75rem; /* 保持和消息气泡一致的左上圆角 */
    border-bottom-left-radius: 0.75rem;
    border-bottom-right-radius: 0.75rem;
}

/* 自身消息的视频容器及视频元素样式 */
.chat-self .video-container video,
.chat-self .video-container .play-button {
    border-top-right-radius: 0; /* 右上直角 */
    border-top-left-radius: 0.75rem; /* 左上圆角 */
    border-bottom-left-radius: 0.75rem;
    border-bottom-right-radius: 0.75rem;
}

/* 修正自身消息气泡与媒体元素的圆角一致性 */
.chat-self .bg-gray-200.mees, /* 自身消息气泡 */
.chat-self .video-container, /* 视频容器外层 */
.chat-self .chat-image {
    border-top-right-radius: 0 !important; /* 强制右上直角 */
    border-top-left-radius: 0.75rem !important; /* 统一左上圆角 */
}
/* 自身消息(靠右)的视频封面样式 */
.chat-self .video-container .video-cover {
    border-top-right-radius: 0 !important; /* 右上直角 */
    border-top-left-radius: 0.75rem !important; /* 左上圆角 */
    border-bottom-left-radius: 0.75rem !important; /* 保持底部圆角 */
    border-bottom-right-radius: 0.75rem !important;
}
/* 左侧消息(他人消息)整体容器 */
.flex.items-start:not(.chat-self) {
    /* 关键:让左侧消息占据左侧空间,右侧自动留白 */
    margin-right: auto; /* 右侧自动填充,消息气泡靠左 */
    max-width: 90%; /* 左侧消息最大宽度 70%(小于 80% 留出右侧空间) */
}

/* 右侧消息(自身消息)整体容器 */
.chat-self {
    /* 关键:让右侧消息占据右侧空间,左侧自动留白 */
    margin-left: auto; /* 左侧自动填充,消息气泡靠右 */
    max-width: 90%; /* 右侧消息最大宽度 70%(对称设置) */
}

/* 消息气泡核心样式(统一调整) */
.bg-gray-200.mees {
    max-width: 100% !important; /* 气泡宽度基于外层容器的 70% */
    width: auto;
    word-wrap: break-word;
    /* 移除之前冲突的全局 max-width,改为由外层容器控制 */
}
/* 原限制宽度的容器改为自动适应 */
.max-w-[75%].flex.flex-col.items-start {
    max-width: 100% !important; /* 取消 75% 固定限制,让外层消息容器(70%)生效 */
    width: fit-content; /* 容器宽度随内容自适应 */
}
/* 极长文本时进一步缩小气泡宽度 */
@media (min-width: 769px) {
    .flex.items-start:not(.chat-self),
    .chat-self {
        max-width: 90%; /* 大屏下可稍宽 */
    }
}

/* 超小屏幕(如手机竖屏)极致适配 */
@media (max-width: 480px) {
    .flex.items-start:not(.chat-self),
    .chat-self {
        max-width: 90%; /* 小屏幕放宽到 90% */
    }
}

/* 确保头像与消息气泡间距合理 */
.avatar-chat {
    width: 25px !important; /* 固定头像宽度,避免影响布局计算 */
    height: 25px !important;
}
/* 聊天框样式(新增/修改部分) */
div#chat-box {
    touch-action: auto; /* 关键修复:允许内部滚动 */
    overscroll-behavior-y: contain; /* 阻止滚动影响页面主体 */
    -webkit-overflow-scrolling: touch; /* 移动端优化 */
    user-select: text; /* 允许内容选择 */
}

#message-list {
    user-select: text; /* 明确消息列表可选择 */
}

/* 禁止非文本区域选择(如头像、时间戳) */
.avatar-chat, .text-xs.text-gray-500, .items-center, .chat-header {
    user-select: none;
}

/* 输入框和发送按钮可交互 */
#message-input {
    user-select: auto; /* 输入框允许文本选择 */
}

/* 防止聊天框外的区域响应滑动事件(可选,根据布局调整) */
.chat-container {
    pointer-events: auto;
}
.meida1 {
    display: flex
;
    align-items: center;
    padding-left: 6px;
    position: relative;
    background-color: #f7f6f1;
}
.mediaItem {
    display: flex
;
    justify-content: center;
    align-items: center;
    box-sizing: border-box;
    margin: 0 5px;
    cursor: pointer;
    pointer-events: auto;
}
i.far.fa-smile {
    font-size: 18px;
}

        /* 表情面板样式 */
        #faceBox {
            position: absolute;
            background: white;
            border: 1px solid rgb(224, 224, 224);
            padding: 5px;
            margin-top: 4px;
            background: #fff;
            padding: 2px;
            border: 1px #dfe6f6 solid;
            border-radius: 10px;
            height: 126px;
            overflow: auto;
            padding: 5px;
            width: 232px;
            top: 341px;
            display: none;
            pointer-events: auto;
        }

        #faceBox img {
            width: 25px;
            height: 25px;
            margin: 2px;
            cursor: pointer;
            border: 0;
            vertical-align: middle;
        }
        /* 消息状态提示 */
.status-text {
    font-size: 0.7rem;
    color: #666;
    margin-top: 4px;
    display: block;
    text-align: left;
}

.status-sending {
    color: #888;
}

.status-success {
    color: green;
}

.status-error {
    color: red;
}
.load-more-indicator {
    text-align: center;
    font-size: 0.75rem;
    color: #666;
    padding: 8px;
    cursor: pointer;
}

.unread-indicator {
    position: absolute;
    top: 8%;
    left: 80%;
    transform: translate(-50%, -50%);
    background-color: #878787;
    color: white;
    border-radius: 10px;
    padding: 2px 5px;
    font-size: 10px;
    z-index: 10;
    cursor: pointer;
}
    </style>
<div class="chat-container">
    <div class="chat-box">
        <div class="chat-header">
            <div>聊天室<span class="beta-icon">beta V3.0</span></div>
            <div>
                <button id="refresh-button">🔄</button>
                <button id="close-button">❎</button>
            </div>
        </div>
        <div id="chat-box" class="main relative">
  <div class="load-more-indicator" id="loadMoreIndicator">点击加载更多历史消息</div>
  <div id="message-list" style="position: relative;"></div>
  <div class="unread-indicator" id="unread-indicator"></div>
  <div id="last-seen-marker" class="last-seen hidden">请勿在此聊天室发送广告</div>
</div>
        <div class="meida1">
            <div class="mediaItem">
                <div id="faceBtn" class="face-btn" title="选择表情">
                    <i class="far fa-smile"></i>
                </div>
            </div>
        </div>
        <div class="flex">
            <input type="text" id="message-input" placeholder="输入消息">
            <button id="send-button" ontouchstart="sendMessage()">发送</button>
        </div>
        <div class="lightbox" id="lightbox">
            <div class="lightbox-content">
                <span class="close-lightbox">×</span>
                <img src="" alt="放大图片" id="lightbox-image">
            </div>
        </div>
        <div id="faceBox" style="position: absolute; background: white; border: 1px solid #e0e0e0; padding: 5px; display: none;"></div>
    </div>
    <div class="chat-icon">
        <i class="fas fa-paper-plane"></i>
    </div>
</div>
<script>
const chatContainer = document.querySelector('.chat-container');
const chatBox = document.querySelector('.chat-box');
const chatIcon = document.querySelector('.chat-icon');
const chatContent = document.getElementById('chat-box');
const messageList = document.getElementById('message-list');
const refreshButton = document.getElementById('refresh-button');
const closeButton = document.getElementById('close-button');
const messageInput = document.getElementById('message-input');
const sendButton = document.getElementById('send-button');
const myUid = '<?php echo $user['uid'];?>';
const facePath = '/plugin/Ty_face/face/arclist/';

let hasUserInteracted = false;
let autoRefreshInterval;
const AUTO_REFRESH_INTERVAL = 5000;
const MESSAGES_PER_PAGE = 30;

let isLoadingMore = false;
let hasMoreMessages = true;
let lastLoadedTimestamp = 0;
let lastSeenTimestamp = parseInt(localStorage.getItem('last_seen_timestamp')) || 0;

// 存储临时消息 { timestamp: DOM Element }
const tempMessages = new Map();
const displayedTimestamps = new Set();

document.addEventListener('visibilitychange', handleVisibilityChange);

function initEmojiPanel() {
  const faceBtn = document.getElementById('faceBtn');
  const faceBox = document.getElementById('faceBox');

  for (let i = 1; i <= 54; i++) {
    const img = document.createElement('img');
    img.src = `${facePath}${i}.gif`;
    img.width = 25;
    img.height = 25;
    img.style.margin = '2px';
    img.style.cursor = 'pointer';
    img.dataset.code = i;
    img.onclick = function () {
      insertEmoji(this.dataset.code);
      faceBox.style.display = 'none';
    };
    faceBox.appendChild(img);
  }

  faceBtn.addEventListener('click', function (e) {
    e.stopPropagation();
    faceBox.style.display = faceBox.style.display === 'none' ? 'block' : 'none';
  });

  document.addEventListener('click', function () {
    faceBox.style.display = 'none';
  });
}

function insertEmoji(code) {
  const cursorPos = messageInput.selectionStart;
  messageInput.value = messageInput.value.substr(0, cursorPos) + `[em_${code}]` + messageInput.value.substr(cursorPos);
  messageInput.selectionStart = messageInput.selectionEnd = cursorPos + `[em_${code}]`.length;
  messageInput.focus();
}

function replaceEmojiCodes(message) {
  return message.replace(/\[em_(\d+)\]/g, function (match, code) {
    return `<img src="${facePath}${code}.gif" width="25px" border="0">`;
  });
}

function formatTime(timestamp) {
  const now = new Date();
  const messageTime = new Date(timestamp);

  // 获取“今天”、“昨天”、“前天”的日期
  const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
  const yesterday = new Date(today);
  yesterday.setDate(today.getDate() - 1);
  const dayBeforeYesterday = new Date(today);
  dayBeforeYesterday.setDate(today.getDate() - 2);

  // 判断是否为今天、昨天、前天
  const isToday = messageTime >= today;
  const isYesterday = messageTime >= yesterday && messageTime < today;
  const isDayBeforeYesterday = messageTime >= dayBeforeYesterday && messageTime < yesterday;

  // 格式化时间部分
  const hours = String(messageTime.getHours()).padStart(2, '0');
  const minutes = String(messageTime.getMinutes()).padStart(2, '0');

  if (now - messageTime < 60000) {
    return '刚刚';
  } else if (isToday) {
    return `${hours}:${minutes}`;
  } else if (isYesterday) {
    return `昨天 ${hours}:${minutes}`;
  } else if (isDayBeforeYesterday) {
    return `前天 ${hours}:${minutes}`;
  } else {
    // 更早的日期显示为“5月7日 23:02”
    const month = messageTime.getMonth() + 1; // 月份从0开始
    const date = messageTime.getDate();
    return `${month}月${date}日 ${hours}:${minutes}`;
  }
}

function getLocalMessages() {
  return JSON.parse(localStorage.getItem('chat_messages') || '[]');
}

function setLocalMessages(messages) {
  localStorage.setItem('chat_messages', JSON.stringify(messages));
}

function showUnreadIndicator(count) {
  const unreadEl = document.getElementById('unread-indicator');
  if (count > 0) {
    unreadEl.textContent = `↑ ${count}条未读`;
    unreadEl.style.display = 'block';
    unreadEl.onclick = () => {
      const firstNewMessage = Array.from(messageList.children).find(el => {
        return el.dataset.timestamp && parseInt(el.dataset.timestamp) > lastSeenTimestamp;
      });
      if (firstNewMessage) {
        firstNewMessage.scrollIntoView({ behavior: 'smooth' });
        localStorage.removeItem('last_seen_timestamp');
        unreadEl.style.display = 'none';
      }
    };
  } else {
    unreadEl.style.display = 'none';
  }
}

function loadChatRecords(isAutoRefresh = false, isLoadMore = false) {
  let lastTimestamp = 0; // ✅ 重置初始值
if (isLoadMore) {
  // 加载更多时,取最早的消息时间戳(最旧的一条)
  const earliestMessage = messageList.firstElementChild;
  if (earliestMessage) {
    lastTimestamp = parseInt(earliestMessage.dataset.timestamp);
  }
} else {
  // 正常加载时,取最新的消息时间戳(最后一条)
  const latestMessage = messageList.lastElementChild;
  if (latestMessage) {
    lastTimestamp = parseInt(latestMessage.dataset.timestamp);
  }
}

  if (!isLoadMore && messageList.children.length > 0) {
    const lastMessage = messageList.lastElementChild;
    if (lastMessage && lastMessage.dataset.timestamp) {
      lastTimestamp = parseInt(lastMessage.dataset.timestamp);
    }
  }

  const xhr = new XMLHttpRequest();
  const url = `/lunbo/chat/load_chat.php?last_timestamp=${lastTimestamp}&limit=${MESSAGES_PER_PAGE}&load_more=${isLoadMore ? 1 : 0}`;
  xhr.open('GET', url, true);
  xhr.onreadystatechange = function () {
    if (xhr.readyState === 4 && xhr.status === 200) {
      try {
        const records = JSON.parse(xhr.responseText);

        if (records.length < MESSAGES_PER_PAGE) {
          hasMoreMessages = false;
        }

        // ✅ 只在加载更多时更新 lastLoadedTimestamp 为最早的一条
        if (isLoadMore && records.length > 0) {
          lastLoadedTimestamp = Date.parse(records[records.length - 1].timestamp); // 最旧的一条
        } else if (!isLoadMore && records.length > 0) {
          lastLoadedTimestamp = Date.parse(records[0].timestamp); // 最新的一条
        }

        // 缓存新消息到 localStorage
        const localMessages = getLocalMessages();
        const newMessages = records.filter(record =>
          !localMessages.some(m => m.client_timestamp == record.client_timestamp)
        );
        setLocalMessages([...newMessages, ...localMessages]);

        let newMessageCount = 0;

        const scrollTopBefore = chatContent.scrollTop;
        const scrollHeightBefore = messageList.scrollHeight;

        records.forEach((record) => {
          const recordTimestamp = Date.parse(record.timestamp);

          if (displayedTimestamps.has(recordTimestamp)) return;
          displayedTimestamps.add(recordTimestamp);

          // 替换临时消息
          if (record.client_timestamp) {
            const tempMessageEl = tempMessages.get(record.client_timestamp);
            if (tempMessageEl) {
              const realMessageEl = createRealMessageElement(record);
              tempMessageEl.replaceWith(realMessageEl);
              tempMessages.delete(record.client_timestamp);
              return;
            }
          }

          // 添加新消息到最前面
          if (!document.querySelector(`[data-timestamp="${recordTimestamp}"]`)) {
            const realMessageEl = createRealMessageElement(record);
            messageList.insertBefore(realMessageEl, messageList.firstChild);
            newMessageCount++;
          }
        });

        // 恢复滚动位置
        if (isLoadMore) {
          const heightDiff = messageList.scrollHeight - scrollHeightBefore;
          chatContent.scrollTop = scrollTopBefore + heightDiff;
        } else if (!isAutoRefresh) {
          chatContent.scrollTop = chatContent.scrollHeight;
        }

        document.getElementById('loadMoreIndicator').textContent =
          hasMoreMessages ? '点击加载更多...' : '已加载所有消息!';

        if (!isLoadMore) {
          showUnreadIndicator(newMessageCount);
        }

      } finally {
        isLoadingMore = false;
      }
    }
  };
  xhr.send();

  return new Promise(resolve => {
    xhr.onload = resolve;
  });
}

function startLongPolling() {
  function poll() {
    loadChatRecords(true).finally(() => {
      setTimeout(poll, 1000);
    });
  }

  poll(); // 启动首次请求
}

function startAutoRefresh() {
  stopAutoRefresh(); // 确保没有旧的定时器
  startLongPolling(); // 使用长轮询
}

function stopAutoRefresh() {
  clearInterval(autoRefreshInterval);
}

function handleVisibilityChange() {
  if (document.visibilityState === 'visible') {
    if (localStorage.getItem('chatBoxState') === 'open') {
      startAutoRefresh();
    }
  } else {
    stopAutoRefresh();
  }
}

// 手动刷新逻辑(不清空已有内容)
refreshButton.addEventListener('click', function () {
  this.style.transform = 'rotate(360deg)';
  this.disabled = true;

  const earliestElements = Array.from(messageList.querySelectorAll('[data-timestamp]'));
  const earliestTimestamp = earliestElements.length
    ? Math.min(...earliestElements.map(el => parseInt(el.dataset.timestamp)))
    : 0;

  const xhr = new XMLHttpRequest();
  xhr.open('GET', `/lunbo/chat/load_chat.php?since=${earliestTimestamp}`, true);
  xhr.onreadystatechange = function () {
    if (xhr.readyState === 4 && xhr.status === 200) {
      try {
        const records = JSON.parse(xhr.responseText);
        records.forEach((record) => {
          const recordTimestamp = Date.parse(record.timestamp);
          if (!displayedTimestamps.has(recordTimestamp)) {
            displayedTimestamps.add(recordTimestamp);
            const realMessageEl = createRealMessageElement(record);
            messageList.insertBefore(realMessageEl, messageList.firstChild);
          }
        });
      } catch (e) {
        console.error("刷新失败", xhr.responseText);
      }
    }
    this.style.transform = 'rotate(0deg)';
    this.disabled = false;
  };
  xhr.send();
});

// 页面加载时恢复缓存消息
window.addEventListener('load', () => {
  initEmojiPanel();
  loadState();
  displayedTimestamps.clear();

  const cachedMessages = getLocalMessages();
  if (cachedMessages.length > 0) {
    // ✅ 插入所有缓存消息
    for (let i = 0; i < cachedMessages.length; i++) {
      const record = cachedMessages[i];
      const recordTimestamp = Date.parse(record.timestamp);
      if (!displayedTimestamps.has(recordTimestamp)) {
        displayedTimestamps.add(recordTimestamp);
        const realMessageEl = createRealMessageElement(record);
        messageList.insertBefore(realMessageEl, messageList.firstChild);
      }
    }

    // 设置最后一条已加载消息的时间戳(最旧的一条)
    lastLoadedTimestamp = Date.parse(cachedMessages[cachedMessages.length - 1].timestamp);

    // 如果缓存数量大于等于一页容量,说明还有更多未加载
    hasMoreMessages = cachedMessages.length >= MESSAGES_PER_PAGE;

    document.getElementById('loadMoreIndicator').textContent =
      hasMoreMessages ? '点击加载更多...' : '已加载所有消息!';

    // 自动滚动到底部
    chatContent.scrollTop = chatContent.scrollHeight;

    // ✅ 自动尝试加载更多缓存的历史消息(可选)
    setTimeout(() => {
      if (hasMoreMessages && !isLoadingMore) {
        isLoadingMore = true;
        const indicator = document.getElementById('loadMoreIndicator');
        indicator.textContent = '加载中...';
        loadChatRecords(false, true).finally(() => {
          indicator.textContent = hasMoreMessages ? '点击加载更多...' : '已加载所有消息!';
        });
      }
    }, 1000); // 延迟执行,避免影响首屏体验
  } else {
    loadChatRecords(); // 没有缓存则正常加载
  }

  chatContent.scrollTop = chatContent.scrollHeight;

  messageList.addEventListener('touchmove', (e) => {
    if (messageList.scrollHeight > messageList.clientHeight) {
      e.preventDefault();
    }
  }, { passive: false });

  document.getElementById('lightbox').addEventListener('click', (e) => {
    if (e.target.classList.contains('lightbox') || e.target.classList.contains('close-lightbox')) {
      e.target.closest('.lightbox').style.display = 'none';
    }
  });

  sendButton.addEventListener('pointerup', sendMessage);
  messageInput.addEventListener('keydown', (e) => {
    if (e.ctrlKey && e.key === 'Enter') {
      e.preventDefault();
      const cursorPos = messageInput.selectionStart;
      messageInput.value = [
        messageInput.value.slice(0, cursorPos),
        '\n',
        messageInput.value.slice(cursorPos)
      ].join('');
      messageInput.setSelectionRange(cursorPos + 1, cursorPos + 1);
    } else if (e.key === 'Enter') {
      e.preventDefault();
      sendMessage(e);
    }
  });

// 禁止自动加载更多
let isAutoLoadingDisabled = true;

// 监听点击事件,触发加载更多
document.getElementById('loadMoreIndicator').addEventListener('click', () => {
  const indicator = document.getElementById('loadMoreIndicator');

  // ✅ 新增防重复点击判断
  if (isLoadingMore || !hasMoreMessages) {
    if (!hasMoreMessages) alert('已经没有更多消息了');
    return;
  }

  isLoadingMore = true;
  indicator.textContent = '加载中...';
  indicator.style.opacity = '0.6';
  indicator.style.pointerEvents = 'none';

  const minLoadingTime = 800;
  const startTime = Date.now();

  loadChatRecords(false, true).finally(() => {
    const duration = Date.now() - startTime;
    setTimeout(() => {
      indicator.textContent = hasMoreMessages ? '点击加载更多...' : '已加载所有消息!';
      indicator.style.opacity = '1';
      indicator.style.pointerEvents = 'auto';
      isLoadingMore = false; // ✅ 恢复加载状态
    }, Math.max(0, minLoadingTime - duration));
  });
});
  });
// 消息发送逻辑
let isSending = false;

function sendMessage(e) {
  if (isSending) return;

  const message = messageInput.value.trim();
  if (!message) {
    showInputError("请输入内容");
    return;
  }
  if (message.length > 140) {
    showInputError("消息长度不能超过140字符");
    return;
  }
  if (/<\/?[\s\S]*?(script|onerror|onload|javascript:)/i.test(message)) {
    showInputError("禁止包含危险脚本");
    return;
  }

  if (e) e.preventDefault();

  const clientTimestamp = Date.now();
  const tempMessage = createTempMessage(message, clientTimestamp);
  messageList.appendChild(tempMessage);
  tempMessages.set(clientTimestamp, tempMessage);

  chatContent.scrollTop = chatContent.scrollHeight;

  isSending = true;
  const xhr = new XMLHttpRequest();
  xhr.open('POST', '/lunbo/chat/save_chat.php', true);
  xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');

  xhr.onload = function () {
    isSending = false;
    if (xhr.status === 200) {
      try {
        const res = JSON.parse(xhr.responseText);
        if (res.status === 'success') {
          messageInput.value = '';
          const tempMessageEl = tempMessages.get(res.client_timestamp);
          if (tempMessageEl) {
            const statusEl = tempMessageEl.querySelector('.status-text');
            if (statusEl) {
              statusEl.textContent = "发送成功!";
              statusEl.className = "status-text status-success";
              setTimeout(() => {
                if (statusEl) statusEl.style.display = 'none';
              }, 1500);
            }

            const realMessageEl = createRealMessageElement({
              uid: myUid,
              username: '<?php echo htmlspecialchars($user['username']);?>',
              avatar_url: '<?php echo $user['avatar_url'];?>',
              message: message,
              timestamp: res.timestamp,
              gid: <?php echo $user['gid'];?>,
              client_timestamp: res.client_timestamp
            });
            tempMessageEl.replaceWith(realMessageEl);
            tempMessages.delete(res.client_timestamp);
          }
        } else {
          const statusEl = tempMessage.querySelector('.status-text');
          if (statusEl) {
            statusEl.textContent = "消息发送失败!";
            statusEl.className = "status-text status-error";
            setTimeout(() => {
              if (statusEl) statusEl.style.display = 'none';
            }, 1500);
          }
          showError("消息发送失败,请重试");
        }
      } catch (e) {
        const statusEl = tempMessage.querySelector('.status-text');
        if (statusEl) {
          statusEl.textContent = "服务器返回异常";
          statusEl.className = "status-text status-error";
          setTimeout(() => {
            if (statusEl) statusEl.style.display = 'none';
          }, 1500);
        }
        showError("服务器返回异常");
      }
    } else {
      const statusEl = tempMessage.querySelector('.status-text');
      if (statusEl) {
        statusEl.textContent = "网络请求失败";
        statusEl.className = "status-text status-error";
        setTimeout(() => {
          if (statusEl) statusEl.style.display = 'none';
        }, 1500);
      }
      showError("网络请求失败");
    }
  };

  xhr.onerror = function () {
    isSending = false;
    const statusEl = tempMessage.querySelector('.status-text');
    if (statusEl) {
      statusEl.textContent = "网络请求失败";
      statusEl.className = "status-text status-error";
      setTimeout(() => {
        if (statusEl) statusEl.style.display = 'none';
      }, 1500);
    }
    showError("网络请求失败");
  };

  xhr.send(`message=${encodeURIComponent(message)}&uid=${myUid}&username=<?php echo htmlspecialchars($user['username']);?>&avatar_url=<?php echo $user['avatar_url'];?>&gid=<?php echo $user['gid'];?>&client_timestamp=${clientTimestamp}`);
}

function createTempMessage(message, clientTimestamp) {
  const messageElement = document.createElement('div');
  messageElement.classList.add('mb-2', 'flex', 'items-start', 'chat-self');
  messageElement.dataset.timestamp = Date.now();
  messageElement.dataset.clientTimestamp = clientTimestamp;

  const currentUsername = '<?php echo htmlspecialchars($user['username']);?>';
  const currentAvatar = '<?php echo htmlspecialchars($user['avatar_url']);?>';
  const currentUid = myUid;
  const currentGid = parseInt('<?php echo $user['gid'];?>', 10);

  let usernameColor = "#868e96";
  if (currentGid === 1 || currentGid === 106) {
    usernameColor = "#c000ff";
  }

  let messageWithBr = message.replace(/\n/g, '<br>');
  messageWithBr = replaceEmojiCodes(messageWithBr);
  messageWithBr = messageWithBr.replace(
    /(?<!["'])(https?:\/\/[^\s]+\.(?:jpg|jpeg|svg|png|gif|bmp|webp)(?:\?[^\s]*)?)(?!["'])/gi,
    '<img data-src="$1" src="$1" alt="图片" class="chat-image" style="max-width: 6rem; height: auto;" onerror="this.src=\'/img/wutu.svg\';">'
  );
  messageWithBr = messageWithBr.replace(
    /(?<!["'])(https?:\/\/(www\.)?[^\s]+\.[^\s]+)(?!["'])(?!\.(?:jpg|jpeg|svg|png|gif|bmp|webp)(?:\?[^\s]*)?)/g,
    '<a href="$1" target="_blank" rel="noopener noreferrer">$1</a>'
  );

  // ✅ 开始:创建昵称链接(代替原来的 innerHTML)
  const avatar = document.createElement('img');
  avatar.className = 'avatar-chat w-8 h-8 rounded-full mr-2';
  avatar.src = currentAvatar;
  avatar.alt = '头像';
  avatar.dataset.username = currentUsername;

  const wrapper = document.createElement('div');
  wrapper.className = 'flex items-start chat-self';
  wrapper.dataset.timestamp = Date.now();

  const messageContainer = document.createElement('div');
  messageContainer.className = 'max-w-[75%] flex flex-col items-start';

  // 昵称链接
  const metaLine = document.createElement('div');
  metaLine.className = 'flex items-center';

  const usernameLink = document.createElement('a');
  usernameLink.href = `/user-${currentUid}.htm`;
  usernameLink.className = 'text-xs text-blue-600 font-semibold mb-1 no-underline';

  const usernameSpan = document.createElement('span');
  usernameSpan.textContent = currentUsername;
  usernameSpan.style.color = usernameColor;

  usernameLink.appendChild(usernameSpan);

  const uidText = document.createElement('span');
  uidText.textContent = `(UID: ${currentUid})`;
  uidText.style.fontSize = '10px';

  metaLine.appendChild(usernameLink);
  metaLine.appendChild(uidText);

  const timeText = document.createElement('span');
  timeText.className = 'text-xs text-gray-500';
  timeText.textContent = '刚刚';

  const contentDiv = document.createElement('div');
  contentDiv.className = 'bg-gray-200 mees';
  contentDiv.innerHTML = messageWithBr;

  const statusText = document.createElement('span');
  statusText.className = 'status-text status-sending';
  statusText.id = `status-${clientTimestamp}`;
  statusText.textContent = '发送中...';

  messageContainer.appendChild(metaLine);
  messageContainer.appendChild(timeText);
  messageContainer.appendChild(contentDiv);
  messageContainer.appendChild(statusText);

  wrapper.appendChild(avatar);
  wrapper.appendChild(messageContainer);

  return wrapper;
}

function createRealMessageElement(record) {
  const recordTimestamp = Date.parse(record.timestamp);

  let usernameColor = "#868e96";
  if (record.gid === 1 || record.gid === 106) {
    usernameColor = "#c000ff";
  }

  let messageWithBr = record.message.replace(/\n/g, '<br>');
  messageWithBr = replaceEmojiCodes(messageWithBr);
  messageWithBr = messageWithBr.replace(
    /(?<!["'])(https?:\/\/[^\s]+\.(?:jpg|jpeg|svg|png|gif|bmp|webp)(?:\?[^\s]*)?)(?!["'])/gi,
    '<img data-src="$1" src="$1" alt="图片" class="chat-image" style="max-width: 6rem; height: auto;" onerror="this.src=\'/img/wutu.svg\';">'
  );
  messageWithBr = messageWithBr.replace(
    /(?<!["'])(https?:\/\/(www\.)?[^\s]+\.[^\s]+)(?!["'])(?!\.(?:jpg|jpeg|svg|png|gif|bmp|webp)(?:\?[^\s]*)?)/g,
    '<a href="$1" target="_blank" rel="noopener noreferrer">$1</a>'
  );

  const safeUsername = DOMPurify.sanitize(record.username, {
    ALLOWED_TAGS: [],
    ALLOWED_ATTR: {}
  });

  // 创建整体消息容器
  const messageElement = document.createElement('div');
  messageElement.classList.add('mb-2', 'flex', 'items-start');
  if (record.uid == myUid) messageElement.classList.add('chat-self');
  messageElement.dataset.timestamp = recordTimestamp;

  // 头像部分
  const avatar = document.createElement('img');
  avatar.className = 'avatar-chat w-8 h-8 rounded-full mr-2';
  avatar.src = record.avatar_url;
  avatar.alt = '头像';
  avatar.dataset.username = safeUsername;

  // 昵称链接部分
  const messageContainer = document.createElement('div');
  messageContainer.className = 'max-w-[75%] flex flex-col items-start';

  const metaLine = document.createElement('div');
  metaLine.className = 'flex items-center';

  const usernameLink = document.createElement('a');
  usernameLink.href = `/user-${record.uid}.htm`;
  usernameLink.className = 'text-xs text-blue-600 font-semibold mb-1 no-underline';

  const usernameSpan = document.createElement('span');
  usernameSpan.textContent = safeUsername;
  usernameSpan.style.color = usernameColor;

  usernameLink.appendChild(usernameSpan);

  const uidText = document.createElement('span');
  uidText.textContent = `(UID: ${record.uid})`;
  uidText.style.fontSize = '10px';

  metaLine.appendChild(usernameLink);
  metaLine.appendChild(uidText);

  // 时间部分
  const timeText = document.createElement('span');
  timeText.className = 'text-xs text-gray-500';
  timeText.textContent = formatTime(recordTimestamp);

  // 消息内容部分
  const contentDiv = document.createElement('div');
  contentDiv.className = 'bg-gray-200 mees';
  contentDiv.innerHTML = messageWithBr;

  // 组装消息内容
  messageContainer.appendChild(metaLine);
  messageContainer.appendChild(timeText);
  messageContainer.appendChild(contentDiv);

  // 把头像 + 内容组合起来
  messageElement.appendChild(avatar);
  messageElement.appendChild(messageContainer);

  // 点击 @ 用户名插入输入框
  avatar.addEventListener('click', () => {
    messageInput.value += `@${safeUsername} `;
    messageInput.focus();
  });

  // 图片点击放大
  const chatImages = messageElement.querySelectorAll('.chat-image');
  chatImages.forEach(img => {
    img.addEventListener('click', () => {
      showLightbox(img.dataset.src || img.src);
    });
  });

  return messageElement;
}

function replaceEmojiCodes(message) {
  return message.replace(/\[em_(\d+)\]/g, function (match, code) {
    return `<img src="${facePath}${code}.gif" width="25px" border="0">`;
  });
}

function showInputError(msg) {
  messageInput.classList.add('border-red-500');
  messageInput.placeholder = msg;
  setTimeout(() => {
    messageInput.classList.remove('border-red-500');
    messageInput.placeholder = "输入消息";
  }, 1500);
}

function showError(msg) {
  const errorDiv = document.createElement('div');
  errorDiv.classList.add('text-red-500', 'text-xs', 'mt-1', 'italic', 'w-full');
  errorDiv.textContent = msg;
  messageList.appendChild(errorDiv);
  setTimeout(() => errorDiv.remove(), 3000);
}

function loadState() {
  const state = localStorage.getItem('chatBoxState');
  if (state === 'open') {
    chatBox.style.display = 'block';
    chatIcon.style.display = 'none';
    startAutoRefresh();
  } else {
    chatBox.style.display = 'none';
    chatIcon.style.display = 'flex';
    stopAutoRefresh();
  }
}

function saveState(state) {
  localStorage.setItem('chatBoxState', state);
}

chatIcon.addEventListener('click', () => {
  chatBox.style.display = 'block';
  chatIcon.style.display = 'none';
  saveState('open');
  displayedTimestamps.clear();
  messageList.innerHTML = '';
  lastSeenTimestamp = 0;
  localStorage.removeItem('last_seen_timestamp');
  loadChatRecords();
  startAutoRefresh();
});

closeButton.addEventListener('click', () => {
  localStorage.setItem('last_seen_timestamp', lastLoadedTimestamp);
  chatBox.style.animation = 'fadeOut 0.3s ease';
  setTimeout(() => {
    chatBox.style.display = 'none';
    chatBox.style.animation = '';
    chatIcon.style.display = 'flex';
    saveState('closed');
    stopAutoRefresh();
  }, 300);
});

function showLightbox(imageUrl) {
  const lightbox = document.getElementById('lightbox');
  const lightboxImage = document.getElementById('lightbox-image');
  lightboxImage.src = imageUrl;
  lightbox.style.display = 'flex';
}
</script>
<?php endif; ?>

你可以简单粗暴把这堆前端垃圾放到 </footer> 前面,然后下面是后端部分:

如果你不想改动前端的文件引用位置,那么可以在你的网站根目录创建文件夹 /lunbo/chat/,两个后端文件都在这里面调用的,如果你没有放在这里目录里,记得修改前端文件的地址。

文件 load_chat.php

<?php
header('Content-Type: application/json');
ini_set('display_errors', 0);

$servername = "localhost";
$username = "";
$password = "";
$dbname = "";

$conn = new mysqli($servername, $username, $password, $dbname);
if ($conn->connect_error) {
    echo json_encode([]);
    exit;
}

$lastTimestamp = isset($_GET['last_timestamp']) ? intval($_GET['last_timestamp']) : 0;
$sinceTimestamp = isset($_GET['since']) ? intval($_GET['since']) : 0;
$loadMore = isset($_GET['load_more']) ? intval($_GET['load_more']) : 0;
$limit = isset($_GET['limit']) ? intval($_GET['limit']) : 30;

$sql = "SELECT * FROM chat_records WHERE timestamp >= ?";
$params = [];
$types = 's';

$thirtyDaysAgo = date('Y-m-d H:i:s', strtotime('-30 days'));
$params[] = $thirtyDaysAgo;

if ($sinceTimestamp > 0) {)
    $sql .= " AND timestamp > ?";
    $params[] = date('Y-m-d H:i:s', $sinceTimestamp / 1000);
    $types .= 's';
} elseif ($loadMore && $lastTimestamp > 0) {
    $sql .= " AND timestamp < ?";
    $params[] = date('Y-m-d H:i:s', $lastTimestamp / 1000);
    $types .= 's';
}

$sql .= " ORDER BY timestamp DESC LIMIT ?";
$params[] = $limit;
$types .= 'i';

$startTime = time();
$maxWaitTime = 25;

do {
    $stmt = $conn->prepare($sql);
    $stmt->bind_param($types, ...$params);
    $stmt->execute();
    $result = $stmt->get_result();

    $newRecords = [];
    while ($row = $result->fetch_assoc()) {
        $row['username'] = htmlspecialchars($row['username'], ENT_QUOTES, 'UTF-8');
        $row['message'] = htmlspecialchars($row['message'], ENT_QUOTES, 'UTF-8');
        $newRecords[] = $row;
    }

    if (!empty($newRecords)) {
        echo json_encode($newRecords);
        exit;
    }

    usleep(500000); 
} while (time() - $startTime < $maxWaitTime);

echo json_encode([]);
$conn->close();
?>

文件 save_chat.php

<?php
header('Content-Type: application/json');

$servername = "localhost";
$username = "";
$password = "";
$dbname = "";

$conn = new mysqli($servername, $username, $password, $dbname);
if ($conn->connect_error) {
    echo json_encode(['status' => 'error', 'message' => '数据库连接失败']);
    exit;
}

$message = isset($_POST['message']) ? trim($_POST['message']) : '';
$uid = isset($_POST['uid']) ? intval($_POST['uid']) : 0;
$username = trim($_POST['username']);
$avatar_url = filter_var(trim($_POST['avatar_url']), FILTER_SANITIZE_URL);
$gid = isset($_POST['gid']) ? intval($_POST['gid']) : 0;
$client_timestamp = isset($_POST['client_timestamp']) ? intval($_POST['client_timestamp']) : 0;

$timestamp = date('Y-m-d H:i:s');

// 白名单标签
$allowedTags = '<br><a><img>';
$message = strip_tags($message, $allowedTags);

// 安全验证
if (preg_match('/<script|javascript:/i', $message)) {
    echo json_encode(['status' => 'error', 'message' => '消息包含危险脚本']);
    exit;
}
if (mb_strlen($message, 'UTF-8') > 1000) {
    echo json_encode(['status' => 'error', 'message' => '消息长度超过限制']);
    exit;
}
if ($uid === 0) {
    echo json_encode(['status' => 'error', 'message' => '用户ID无效']);
    exit;
}

$sql = "INSERT INTO chat_records (uid, username, avatar_url, message, timestamp, gid, client_timestamp) 
        VALUES (?, ?, ?, ?, ?, ?, ?)";
$stmt = $conn->prepare($sql);
$stmt->bind_param("issssss", $uid, $username, $avatar_url, $message, $timestamp, $gid, $client_timestamp);

if ($stmt->execute()) {
    echo json_encode([
        'status' => 'success',
        'client_timestamp' => $client_timestamp,
        'timestamp' => $timestamp,
        'insert_id' => $stmt->insert_id
    ]);
} else {
    echo json_encode(['status' => 'error', 'message' => '写入数据库失败']);
}

$conn->close();
?>

记得编辑两个文件的数据库位置。

创建数据库

创建一个数据库,执行 SQL

CREATE TABLE chat_records (
    id INT AUTO_INCREMENT PRIMARY KEY,
    uid INT NOT NULL,
    username VARCHAR(255) NOT NULL,
    avatar_url VARCHAR(255),
    message TEXT NOT NULL,
    timestamp DATETIME NOT NULL,
    gid INT
);

完毕。

简单说明:

发送图片:直接发送图片链接

发送超链接:直接发送带有协议头的链接

艾特别人:点击头像

支持未读消息记录

支持加载历史消息(默认显示 30 条)

添加了管理组和会员用户组的彩色昵称,这里用的判断用户组 ID 的形式,请根据需求修改 GID

忘了说,表情功能是调用/plugin/Ty_face/face/arclist/目录下的表情,与表情插件无关,只是单纯的调用图片而已。你可以修改为你的目录。

本站资源均为作者提供和网友推荐收集整理而来,仅供学习和研究使用,请在下载后24小时内删除,谢谢合作!
xiuno 的内置聊天室(粗暴版本/非插件版本)|不死鸟资源网
xiuno 的内置聊天室(粗暴版本/非插件版本)
此内容为免费阅读,请登录后查看
¥0
限时特惠
¥99
文章采用CC BY-NC-SA 4.0许可协议授权
免费阅读
THE END
点赞7 分享