迁移聊天室前端工具并优化消息渲染
This commit is contained in:
@@ -83,7 +83,7 @@
|
||||
</select>
|
||||
<input id="mob-user-search-input" type="text" placeholder="搜索用户..."
|
||||
style="flex:2;font-size:11px;border:1px solid #b0c8e0;border-radius:3px;padding:2px 6px;color:#333;"
|
||||
oninput="renderMobileUserList()">
|
||||
oninput="scheduleRenderMobileUserList()">
|
||||
</div>
|
||||
|
||||
{{-- 用户列表容器 --}}
|
||||
@@ -127,6 +127,10 @@
|
||||
* @type {string|null}
|
||||
*/
|
||||
let _mobileDrawerOpen = null;
|
||||
let _mobileUserListRenderTimer = null;
|
||||
let _mobileRoomsOnlineStatusCache = null;
|
||||
let _mobileRoomsOnlineStatusCacheAt = 0;
|
||||
const MOBILE_ROOMS_ONLINE_STATUS_CACHE_TTL = 10000;
|
||||
|
||||
/**
|
||||
* 打开指定抽屉
|
||||
@@ -228,6 +232,21 @@
|
||||
if (footerEl) footerEl.textContent = count;
|
||||
}
|
||||
|
||||
/**
|
||||
* 调度手机端在线名单渲染,避免搜索输入时同步重建整份名单。
|
||||
*/
|
||||
function scheduleRenderMobileUserList() {
|
||||
if (_mobileUserListRenderTimer !== null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const scheduleRender = window.requestAnimationFrame || ((callback) => window.setTimeout(callback, 16));
|
||||
_mobileUserListRenderTimer = scheduleRender(() => {
|
||||
_mobileUserListRenderTimer = null;
|
||||
renderMobileUserList();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 拉取房间列表并渲染到手机端房间容器
|
||||
*/
|
||||
@@ -235,34 +254,64 @@
|
||||
const container = document.getElementById('mob-rooms-online-list');
|
||||
if (!container) return;
|
||||
|
||||
if (_mobileRoomsOnlineStatusCache && Date.now() - _mobileRoomsOnlineStatusCacheAt < MOBILE_ROOMS_ONLINE_STATUS_CACHE_TTL) {
|
||||
renderMobileRoomList(_mobileRoomsOnlineStatusCache, container);
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = '<div style="text-align:center;color:#aaa;padding:16px;font-size:11px;">加载中...</div>';
|
||||
|
||||
fetch('{{ route('chat.rooms-online-status') }}')
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (!data.rooms || !data.rooms.length) {
|
||||
container.innerHTML = '<div style="text-align:center;color:#bbb;padding:16px;font-size:11px;">暂无房间</div>';
|
||||
return;
|
||||
}
|
||||
const currentRoomId = window.chatContext?.roomId;
|
||||
const roomRows = data.rooms.map(room => {
|
||||
const roomId = Number.parseInt(room.id, 10);
|
||||
if (!Number.isInteger(roomId)) {
|
||||
return '';
|
||||
}
|
||||
_mobileRoomsOnlineStatusCache = data;
|
||||
_mobileRoomsOnlineStatusCacheAt = Date.now();
|
||||
renderMobileRoomList(data, container);
|
||||
})
|
||||
.catch(() => {
|
||||
container.innerHTML = '<div style="text-align:center;color:#f00;padding:10px;font-size:11px;">加载失败</div>';
|
||||
});
|
||||
}
|
||||
|
||||
const isCurrent = roomId === currentRoomId;
|
||||
const bg = isCurrent ? '#ecf4ff' : '#fff';
|
||||
const nameColor = isCurrent ? '#336699' : (room.door_open ? '#444' : '#bbb');
|
||||
const safeRoomName = escapeMobileDrawerHtml(String(room.name ?? ''));
|
||||
const safeOnlineCount = Math.max(Number.parseInt(room.online, 10) || 0, 0);
|
||||
const badge = safeOnlineCount > 0
|
||||
? `<span style="background:#e8f5e9;color:#2e7d32;border-radius:8px;padding:0 6px;font-size:10px;font-weight:bold;">${safeOnlineCount}人</span>`
|
||||
: `<span style="background:#f5f5f5;color:#bbb;border-radius:8px;padding:0 6px;font-size:10px;">空</span>`;
|
||||
const currentTag = isCurrent ? `<span style="font-size:9px;color:#7090b0;margin-left:3px;">当前</span>` : '';
|
||||
const clickAttr = isCurrent ? '' : `onclick="location.href='/room/${roomId}'"`;
|
||||
/**
|
||||
* 渲染手机端房间列表。
|
||||
*
|
||||
* @param {Object} data 接口返回数据
|
||||
* @param {HTMLElement} container 目标容器
|
||||
*/
|
||||
function renderMobileRoomList(data, container) {
|
||||
if (window.ChatRoomTools?.renderRoomsOnlineStatusToContainer) {
|
||||
window.ChatRoomTools.renderRoomsOnlineStatusToContainer(data, container, {
|
||||
currentRoomId: window.chatContext?.roomId,
|
||||
variant: 'mobile',
|
||||
emptyHtml: '<div style="text-align:center;color:#bbb;padding:16px;font-size:11px;">暂无房间</div>',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
return `<div ${clickAttr}
|
||||
if (!data.rooms || !data.rooms.length) {
|
||||
container.innerHTML = '<div style="text-align:center;color:#bbb;padding:16px;font-size:11px;">暂无房间</div>';
|
||||
return;
|
||||
}
|
||||
const currentRoomId = window.chatContext?.roomId;
|
||||
const roomRows = data.rooms.map(room => {
|
||||
const roomId = Number.parseInt(room.id, 10);
|
||||
if (!Number.isInteger(roomId)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const isCurrent = roomId === currentRoomId;
|
||||
const bg = isCurrent ? '#ecf4ff' : '#fff';
|
||||
const nameColor = isCurrent ? '#336699' : (room.door_open ? '#444' : '#bbb');
|
||||
const safeRoomName = escapeMobileDrawerHtml(String(room.name ?? ''));
|
||||
const safeOnlineCount = Math.max(Number.parseInt(room.online, 10) || 0, 0);
|
||||
const badge = safeOnlineCount > 0
|
||||
? `<span style="background:#e8f5e9;color:#2e7d32;border-radius:8px;padding:0 6px;font-size:10px;font-weight:bold;">${safeOnlineCount}人</span>`
|
||||
: `<span style="background:#f5f5f5;color:#bbb;border-radius:8px;padding:0 6px;font-size:10px;">空</span>`;
|
||||
const currentTag = isCurrent ? `<span style="font-size:9px;color:#7090b0;margin-left:3px;">当前</span>` : '';
|
||||
const clickAttr = isCurrent ? '' : `onclick="location.href='/room/${roomId}'"`;
|
||||
|
||||
return `<div ${clickAttr}
|
||||
style="display:flex;align-items:center;justify-content:space-between;
|
||||
padding:6px 10px;border-bottom:1px solid #eef2f8;background:${bg};
|
||||
cursor:${isCurrent ? 'default' : 'pointer'};">
|
||||
@@ -270,13 +319,9 @@
|
||||
${safeRoomName}${currentTag}
|
||||
</span>${badge}
|
||||
</div>`;
|
||||
}).filter(Boolean).join('');
|
||||
}).filter(Boolean).join('');
|
||||
|
||||
container.innerHTML = roomRows || '<div style="text-align:center;color:#bbb;padding:16px;font-size:11px;">暂无房间</div>';
|
||||
})
|
||||
.catch(() => {
|
||||
container.innerHTML = '<div style="text-align:center;color:#f00;padding:10px;font-size:11px;">加载失败</div>';
|
||||
});
|
||||
container.innerHTML = roomRows || '<div style="text-align:center;color:#bbb;padding:16px;font-size:11px;">暂无房间</div>';
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
</select>
|
||||
</div>
|
||||
<div style="display:flex; align-items:center; gap:2px;">
|
||||
<input type="text" id="user-search-input" placeholder="搜索用户" onkeyup="filterUserList()"
|
||||
<input type="text" id="user-search-input" placeholder="搜索用户" oninput="scheduleFilterUserList()"
|
||||
style="width:100%; font-size:11px; padding:2px 4px; border:1px solid #aac; border-radius:2px; box-sizing:border-box;">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1420,8 +1420,8 @@
|
||||
}
|
||||
|
||||
// ── 页面刷新后恢复婚礼红包领取按钮 ─────────────────────────
|
||||
// 延迟 2 秒以确保聊天框和 Alpine 均已完成初始化
|
||||
setTimeout(async () => {
|
||||
// 空闲时再查待领取红包,避免和聊天室首屏消息/名单初始化抢网络。
|
||||
window.deferChatGameBootstrap(async () => {
|
||||
try {
|
||||
const res = await fetch('/wedding/pending-envelopes', {
|
||||
headers: {
|
||||
@@ -1467,6 +1467,6 @@
|
||||
} catch (e) {
|
||||
console.warn('[婚礼红包] 恢复待领取按钮失败', e);
|
||||
}
|
||||
}, 2000);
|
||||
}, 3000);
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -37,9 +37,9 @@
|
||||
const toUserSelect = document.getElementById('to_user');
|
||||
const onlineCount = document.getElementById('online-count');
|
||||
const onlineCountBottom = document.getElementById('online-count-bottom');
|
||||
const BLOCKED_SYSTEM_SENDERS_STORAGE_KEY = 'chat_blocked_system_senders';
|
||||
const CHAT_SOUND_MUTED_STORAGE_KEY = 'chat_sound_muted';
|
||||
const BLOCKABLE_SYSTEM_SENDERS = ['钓鱼播报', '星海小博士', '百家乐', '跑马', '神秘箱子'];
|
||||
const BLOCKED_SYSTEM_SENDERS_STORAGE_KEY = window.ChatRoomTools?.BLOCKED_SYSTEM_SENDERS_STORAGE_KEY || 'chat_blocked_system_senders';
|
||||
const CHAT_SOUND_MUTED_STORAGE_KEY = window.ChatRoomTools?.CHAT_SOUND_MUTED_STORAGE_KEY || 'chat_sound_muted';
|
||||
const BLOCKABLE_SYSTEM_SENDERS = window.ChatRoomTools?.BLOCKABLE_SYSTEM_SENDERS || ['钓鱼播报', '星海小博士', '百家乐', '跑马', '神秘箱子'];
|
||||
const hoverTooltip = document.getElementById('chat-hover-tooltip');
|
||||
let activeTooltipTrigger = null;
|
||||
|
||||
@@ -75,6 +75,13 @@
|
||||
let userBadgeRotationTick = 0;
|
||||
let userListRenderTimer = null;
|
||||
let _maxMsgId = 0; // 记录当前收到的最大消息 ID
|
||||
let pendingChatMessages = [];
|
||||
let chatMessageFlushTimer = null;
|
||||
let userFilterRenderTimer = null;
|
||||
let lastAutosaveNode = null;
|
||||
const CHAT_MESSAGE_FLUSH_BATCH_SIZE = 8;
|
||||
const PUBLIC_MESSAGE_NODE_LIMIT = 600;
|
||||
const PRIVATE_MESSAGE_NODE_LIMIT = 300;
|
||||
const initialChatPreferences = normalizeChatPreferences(window.chatContext?.chatPreferences || {});
|
||||
let blockedSystemSenders = new Set(initialChatPreferences.blocked_system_senders);
|
||||
|
||||
@@ -85,6 +92,10 @@
|
||||
* @returns {Object}
|
||||
*/
|
||||
function normalizeChatPreferences(raw) {
|
||||
if (window.ChatRoomTools?.normalizeChatPreferences) {
|
||||
return window.ChatRoomTools.normalizeChatPreferences(raw, BLOCKABLE_SYSTEM_SENDERS);
|
||||
}
|
||||
|
||||
const blocked = Array.isArray(raw?.blocked_system_senders)
|
||||
? raw.blocked_system_senders.filter(sender => BLOCKABLE_SYSTEM_SENDERS.includes(sender))
|
||||
: [];
|
||||
@@ -103,6 +114,10 @@
|
||||
* @returns {Date|null}
|
||||
*/
|
||||
function parseDailyStatusExpiry(expiresAt) {
|
||||
if (window.ChatRoomTools?.parseDailyStatusExpiry) {
|
||||
return window.ChatRoomTools.parseDailyStatusExpiry(expiresAt);
|
||||
}
|
||||
|
||||
if (!expiresAt) {
|
||||
return null;
|
||||
}
|
||||
@@ -119,6 +134,10 @@
|
||||
* @returns {Object|null}
|
||||
*/
|
||||
function normalizeDailyStatus(raw) {
|
||||
if (window.ChatRoomTools?.normalizeDailyStatus) {
|
||||
return window.ChatRoomTools.normalizeDailyStatus(raw);
|
||||
}
|
||||
|
||||
if (!raw || typeof raw !== 'object') {
|
||||
return null;
|
||||
}
|
||||
@@ -1459,6 +1478,9 @@
|
||||
|
||||
// ── Tab 切换 ──────────────────────────────────────
|
||||
let _roomsRefreshTimer = null;
|
||||
let _roomsOnlineStatusCache = null;
|
||||
let _roomsOnlineStatusCacheAt = 0;
|
||||
const ROOMS_ONLINE_STATUS_CACHE_TTL = 10000;
|
||||
|
||||
function switchTab(tab) {
|
||||
// 切换名单/房间 面板
|
||||
@@ -1470,7 +1492,7 @@
|
||||
if (tab === 'rooms') {
|
||||
loadRoomsOnlineStatus();
|
||||
clearInterval(_roomsRefreshTimer);
|
||||
_roomsRefreshTimer = setInterval(loadRoomsOnlineStatus, 30000);
|
||||
_roomsRefreshTimer = setInterval(() => loadRoomsOnlineStatus(true), 30000);
|
||||
} else {
|
||||
clearInterval(_roomsRefreshTimer);
|
||||
_roomsRefreshTimer = null;
|
||||
@@ -1482,42 +1504,72 @@
|
||||
*/
|
||||
const _currentRoomId = {{ $room->id }};
|
||||
|
||||
function loadRoomsOnlineStatus() {
|
||||
function loadRoomsOnlineStatus(forceRefresh = false) {
|
||||
const container = document.getElementById('rooms-online-list');
|
||||
if (!container) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!forceRefresh && _roomsOnlineStatusCache && Date.now() - _roomsOnlineStatusCacheAt < ROOMS_ONLINE_STATUS_CACHE_TTL) {
|
||||
renderRoomsOnlineStatus(_roomsOnlineStatusCache, container);
|
||||
return;
|
||||
}
|
||||
|
||||
fetch('{{ route('chat.rooms-online-status') }}')
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (!data.rooms || !data.rooms.length) {
|
||||
container.innerHTML =
|
||||
'<div style="text-align:center;color:#bbb;padding:16px 0;font-size:11px;">暂无房间</div>';
|
||||
return;
|
||||
}
|
||||
const roomRows = data.rooms.map(room => {
|
||||
const roomId = Number.parseInt(room.id, 10);
|
||||
if (!Number.isInteger(roomId)) {
|
||||
return '';
|
||||
}
|
||||
_roomsOnlineStatusCache = data;
|
||||
_roomsOnlineStatusCacheAt = Date.now();
|
||||
renderRoomsOnlineStatus(data, container);
|
||||
})
|
||||
.catch(() => {
|
||||
container.innerHTML =
|
||||
'<div style="text-align:center;color:#f00;padding:10px;font-size:11px;">加载失败</div>';
|
||||
});
|
||||
}
|
||||
|
||||
const isCurrent = roomId === _currentRoomId;
|
||||
const closed = !room.door_open;
|
||||
const safeRoomName = escapeHtml(String(room.name ?? ''));
|
||||
const safeOnlineCount = Math.max(Number.parseInt(room.online, 10) || 0, 0);
|
||||
const bg = isCurrent ? '#ecf4ff' : '#fff';
|
||||
const border = isCurrent ? '#aac5f0' : '#e0eaf5';
|
||||
const nameColor = isCurrent ? '#336699' : (closed ? '#bbb' : '#444');
|
||||
const badge = safeOnlineCount > 0 ?
|
||||
`<span style="background:#e8f5e9;color:#2e7d32;border-radius:8px;padding:0 5px;font-size:10px;font-weight:bold;white-space:nowrap;flex-shrink:0;">${safeOnlineCount} 人</span>` :
|
||||
`<span style="background:#f5f5f5;color:#bbb;border-radius:8px;padding:0 5px;font-size:10px;white-space:nowrap;flex-shrink:0;">空</span>`;
|
||||
const currentTag = isCurrent ?
|
||||
`<span style="font-size:9px;color:#336699;opacity:.7;margin-left:3px;">当前</span>` :
|
||||
'';
|
||||
const clickHandler = isCurrent ? '' : `onclick="location.href='/room/${roomId}'"`;
|
||||
/**
|
||||
* 渲染房间在线状态列表。
|
||||
*
|
||||
* @param {Object} data 接口返回数据
|
||||
* @param {HTMLElement} container 目标容器
|
||||
*/
|
||||
function renderRoomsOnlineStatus(data, container) {
|
||||
if (window.ChatRoomTools?.renderRoomsOnlineStatusToContainer) {
|
||||
window.ChatRoomTools.renderRoomsOnlineStatusToContainer(data, container, {
|
||||
currentRoomId: _currentRoomId,
|
||||
variant: 'desktop',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
return `<div ${clickHandler}
|
||||
if (!data.rooms || !data.rooms.length) {
|
||||
container.innerHTML =
|
||||
'<div style="text-align:center;color:#bbb;padding:16px 0;font-size:11px;">暂无房间</div>';
|
||||
return;
|
||||
}
|
||||
const roomRows = data.rooms.map(room => {
|
||||
const roomId = Number.parseInt(room.id, 10);
|
||||
if (!Number.isInteger(roomId)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const isCurrent = roomId === _currentRoomId;
|
||||
const closed = !room.door_open;
|
||||
const safeRoomName = escapeHtml(String(room.name ?? ''));
|
||||
const safeOnlineCount = Math.max(Number.parseInt(room.online, 10) || 0, 0);
|
||||
const bg = isCurrent ? '#ecf4ff' : '#fff';
|
||||
const border = isCurrent ? '#aac5f0' : '#e0eaf5';
|
||||
const nameColor = isCurrent ? '#336699' : (closed ? '#bbb' : '#444');
|
||||
const badge = safeOnlineCount > 0 ?
|
||||
`<span style="background:#e8f5e9;color:#2e7d32;border-radius:8px;padding:0 5px;font-size:10px;font-weight:bold;white-space:nowrap;flex-shrink:0;">${safeOnlineCount} 人</span>` :
|
||||
`<span style="background:#f5f5f5;color:#bbb;border-radius:8px;padding:0 5px;font-size:10px;white-space:nowrap;flex-shrink:0;">空</span>`;
|
||||
const currentTag = isCurrent ?
|
||||
`<span style="font-size:9px;color:#336699;opacity:.7;margin-left:3px;">当前</span>` :
|
||||
'';
|
||||
const clickHandler = isCurrent ? '' : `onclick="location.href='/room/${roomId}'"`;
|
||||
|
||||
return `<div ${clickHandler}
|
||||
style="display:flex;align-items:center;justify-content:space-between;
|
||||
padding:5px 8px;margin:2px 3px;border-radius:5px;
|
||||
border:1px solid ${border};background:${bg};
|
||||
@@ -1530,15 +1582,10 @@
|
||||
</span>
|
||||
${badge}
|
||||
</div>`;
|
||||
}).filter(Boolean).join('');
|
||||
}).filter(Boolean).join('');
|
||||
|
||||
container.innerHTML = roomRows ||
|
||||
'<div style="text-align:center;color:#bbb;padding:16px 0;font-size:11px;">暂无房间</div>';
|
||||
})
|
||||
.catch(() => {
|
||||
container.innerHTML =
|
||||
'<div style="text-align:center;color:#f00;padding:10px;font-size:11px;">加载失败</div>';
|
||||
});
|
||||
container.innerHTML = roomRows ||
|
||||
'<div style="text-align:center;color:#bbb;padding:16px 0;font-size:11px;">暂无房间</div>';
|
||||
}
|
||||
|
||||
|
||||
@@ -2215,11 +2262,34 @@
|
||||
|
||||
// 名单中多种徽标轮换展示时,每 3 秒只刷新徽标槽位,不重建头像行。
|
||||
window.setInterval(() => {
|
||||
if (document.hidden) {
|
||||
return;
|
||||
}
|
||||
|
||||
userBadgeRotationTick = (userBadgeRotationTick + 1) % 4;
|
||||
refreshRenderedUserBadges();
|
||||
refreshRenderedUserBadges(userList);
|
||||
const mobileUsersList = document.getElementById('mob-online-users-list');
|
||||
if (mobileUsersList?.offsetParent !== null) {
|
||||
refreshRenderedUserBadges(mobileUsersList);
|
||||
}
|
||||
syncDailyStatusUi();
|
||||
}, 3000);
|
||||
|
||||
/**
|
||||
* 调度用户列表搜索过滤,避免每个按键都同步扫描名单 DOM。
|
||||
*/
|
||||
function scheduleFilterUserList() {
|
||||
if (userFilterRenderTimer !== null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const scheduleFilter = window.requestAnimationFrame || ((callback) => window.setTimeout(callback, 16));
|
||||
userFilterRenderTimer = scheduleFilter(() => {
|
||||
userFilterRenderTimer = null;
|
||||
filterUserList();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 搜索/过滤用户列表
|
||||
*/
|
||||
@@ -2246,7 +2316,7 @@
|
||||
/**
|
||||
* 追加消息到聊天窗格(原版风格:非气泡模式,逐行显示)
|
||||
*/
|
||||
function appendMessage(msg) {
|
||||
function appendMessage(msg, renderBatch = null) {
|
||||
// 记录拉取到的最大消息ID,用于本地清屏功能
|
||||
if (msg && msg.id > _maxMsgId) {
|
||||
_maxMsgId = msg.id;
|
||||
@@ -2524,8 +2594,11 @@
|
||||
if (msg.welcome_user) {
|
||||
div.setAttribute('data-system-user', msg.welcome_user);
|
||||
// 收到后端来的新欢迎消息时,把界面上该用户旧的都删掉
|
||||
const oldWelcomes = container.querySelectorAll(`[data-system-user="${msg.welcome_user}"]`);
|
||||
const welcomeSelector = `[data-system-user="${msg.welcome_user}"]`;
|
||||
const oldWelcomes = container.querySelectorAll(welcomeSelector);
|
||||
oldWelcomes.forEach(el => el.remove());
|
||||
renderBatch?.publicFragment.querySelectorAll(welcomeSelector).forEach(el => el.remove());
|
||||
renderBatch?.privateFragment.querySelectorAll(welcomeSelector).forEach(el => el.remove());
|
||||
}
|
||||
|
||||
// 路由规则(复刻原版):
|
||||
@@ -2545,18 +2618,136 @@
|
||||
if (isRelatedToMe) {
|
||||
// 删除旧的存点通知,保持包厢窗口整洁
|
||||
if (isAutoSave) {
|
||||
container2.querySelectorAll('[data-autosave="1"]').forEach(el => el.remove());
|
||||
lastAutosaveNode?.remove();
|
||||
lastAutosaveNode = div;
|
||||
}
|
||||
if (renderBatch) {
|
||||
renderBatch.privateFragment.appendChild(div);
|
||||
renderBatch.shouldPrunePrivate = true;
|
||||
renderBatch.shouldScrollPrivate = renderBatch.shouldScrollPrivate || autoScroll;
|
||||
return;
|
||||
}
|
||||
container2.appendChild(div);
|
||||
pruneMessageContainer(container2, PRIVATE_MESSAGE_NODE_LIMIT);
|
||||
if (autoScroll) {
|
||||
container2.scrollTop = container2.scrollHeight;
|
||||
}
|
||||
} else {
|
||||
if (renderBatch) {
|
||||
renderBatch.publicFragment.appendChild(div);
|
||||
renderBatch.shouldPrunePublic = true;
|
||||
renderBatch.shouldScrollPublic = renderBatch.shouldScrollPublic || autoScroll;
|
||||
return;
|
||||
}
|
||||
container.appendChild(div);
|
||||
pruneMessageContainer(container, PUBLIC_MESSAGE_NODE_LIMIT);
|
||||
scrollToBottom();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 裁剪聊天窗口内过旧的消息节点,防止长时间在线后 DOM 无限膨胀。
|
||||
*
|
||||
* @param {HTMLElement} targetContainer 聊天窗口容器
|
||||
* @param {number} maxNodes 最大保留节点数
|
||||
*/
|
||||
function pruneMessageContainer(targetContainer, maxNodes) {
|
||||
if (!targetContainer || targetContainer.childElementCount <= maxNodes) {
|
||||
return;
|
||||
}
|
||||
|
||||
while (targetContainer.childElementCount > maxNodes) {
|
||||
const firstNode = targetContainer.firstElementChild;
|
||||
if (firstNode === lastAutosaveNode) {
|
||||
lastAutosaveNode = null;
|
||||
}
|
||||
firstNode?.remove();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建聊天消息批量渲染上下文,集中提交 DOM 变更。
|
||||
*
|
||||
* @returns {Object}
|
||||
*/
|
||||
function createChatMessageRenderBatch() {
|
||||
return {
|
||||
publicFragment: document.createDocumentFragment(),
|
||||
privateFragment: document.createDocumentFragment(),
|
||||
shouldPrunePublic: false,
|
||||
shouldPrunePrivate: false,
|
||||
shouldScrollPublic: false,
|
||||
shouldScrollPrivate: false,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交批量渲染的聊天消息,并把裁剪和滚动合并到每批一次。
|
||||
*
|
||||
* @param {Object} renderBatch 批量渲染上下文
|
||||
*/
|
||||
function commitChatMessageRenderBatch(renderBatch) {
|
||||
const hasPublicMessages = renderBatch.publicFragment.childNodes.length > 0;
|
||||
const hasPrivateMessages = renderBatch.privateFragment.childNodes.length > 0;
|
||||
|
||||
if (hasPublicMessages) {
|
||||
container.appendChild(renderBatch.publicFragment);
|
||||
}
|
||||
if (hasPrivateMessages) {
|
||||
container2.appendChild(renderBatch.privateFragment);
|
||||
}
|
||||
if (renderBatch.shouldPrunePublic) {
|
||||
pruneMessageContainer(container, PUBLIC_MESSAGE_NODE_LIMIT);
|
||||
}
|
||||
if (renderBatch.shouldPrunePrivate) {
|
||||
pruneMessageContainer(container2, PRIVATE_MESSAGE_NODE_LIMIT);
|
||||
}
|
||||
if (renderBatch.shouldScrollPublic) {
|
||||
container.scrollTop = container.scrollHeight;
|
||||
}
|
||||
if (renderBatch.shouldScrollPrivate) {
|
||||
container2.scrollTop = container2.scrollHeight;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将广播消息放入轻量队列,避免连续广播时同步挤占同一帧的 DOM 渲染。
|
||||
*/
|
||||
function enqueueChatMessage(msg) {
|
||||
// 本地清屏依赖最大消息 ID,需要在进入队列时先同步,避免延后渲染导致状态滞后。
|
||||
if (msg && msg.id > _maxMsgId) {
|
||||
_maxMsgId = msg.id;
|
||||
}
|
||||
|
||||
pendingChatMessages.push(msg);
|
||||
|
||||
if (chatMessageFlushTimer !== null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const scheduleFlush = window.requestAnimationFrame || ((callback) => window.setTimeout(callback, 16));
|
||||
chatMessageFlushTimer = scheduleFlush(flushQueuedChatMessages);
|
||||
}
|
||||
|
||||
/**
|
||||
* 分批渲染待处理消息,给动画、输入和滚动留出主线程时间。
|
||||
*/
|
||||
function flushQueuedChatMessages() {
|
||||
chatMessageFlushTimer = null;
|
||||
|
||||
const batch = pendingChatMessages.splice(0, CHAT_MESSAGE_FLUSH_BATCH_SIZE);
|
||||
const renderBatch = createChatMessageRenderBatch();
|
||||
batch.forEach((msg) => appendMessage(msg, renderBatch));
|
||||
commitChatMessageRenderBatch(renderBatch);
|
||||
|
||||
if (pendingChatMessages.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const scheduleFlush = window.requestAnimationFrame || ((callback) => window.setTimeout(callback, 16));
|
||||
chatMessageFlushTimer = scheduleFlush(flushQueuedChatMessages);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将消息追加函数暴露到全局,供页面首次加载时回填历史消息使用。
|
||||
*/
|
||||
@@ -2662,7 +2853,7 @@
|
||||
.chatContext.username) {
|
||||
return;
|
||||
}
|
||||
appendMessage(msg);
|
||||
enqueueChatMessage(msg);
|
||||
|
||||
if (msg.action === 'vip_presence') {
|
||||
showVipPresenceBanner(msg);
|
||||
@@ -2814,6 +3005,7 @@
|
||||
}
|
||||
});
|
||||
}
|
||||
lastAutosaveNode = say2?.querySelector('[data-autosave="1"]') || null;
|
||||
|
||||
// 显示清屏提示
|
||||
const sysDiv = document.createElement('div');
|
||||
@@ -3067,38 +3259,9 @@
|
||||
window.handleFeatureLocalClear = handleFeatureLocalClear;
|
||||
syncDailyStatusUi();
|
||||
|
||||
// ── 字号设置(持久化到 localStorage)─────────────────
|
||||
/**
|
||||
* 应用字号到聊天消息窗口,并保存到 localStorage
|
||||
*
|
||||
* @param {string|number} size 字号大小(px 数字)
|
||||
*/
|
||||
function applyFontSize(size) {
|
||||
const px = parseInt(size, 10);
|
||||
if (isNaN(px) || px < 10 || px > 30) return;
|
||||
|
||||
// 同时应用到公聊窗和包厢窗
|
||||
const c1 = document.getElementById('chat-messages-container');
|
||||
const c2 = document.getElementById('chat-messages-container2');
|
||||
if (c1) c1.style.fontSize = px + 'px';
|
||||
if (c2) c2.style.fontSize = px + 'px';
|
||||
|
||||
// 持久化(key 带房间 ID,不同房间各自记住)
|
||||
const key = 'chat_font_size';
|
||||
localStorage.setItem(key, px);
|
||||
|
||||
// 同步 select 显示
|
||||
const sel = document.getElementById('font_size_select');
|
||||
if (sel) sel.value = String(px);
|
||||
}
|
||||
window.applyFontSize = applyFontSize;
|
||||
|
||||
// 页面加载后从 localStorage 恢复之前保存的字号
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const saved = localStorage.getItem('chat_font_size');
|
||||
if (saved) {
|
||||
applyFontSize(saved);
|
||||
}
|
||||
window.ChatRoomTools?.restoreChatFontSize?.();
|
||||
|
||||
const storedBlockedSystemSenders = loadBlockedSystemSenders();
|
||||
const mutedFromLocal = localStorage.getItem(CHAT_SOUND_MUTED_STORAGE_KEY) === '1';
|
||||
@@ -3290,55 +3453,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 打开聊天图片大图预览层。
|
||||
*/
|
||||
function openChatImageLightbox(imageUrl, imageName = '聊天图片') {
|
||||
const lightbox = document.getElementById('chat-image-lightbox');
|
||||
const imageEl = document.getElementById('chat-image-lightbox-img');
|
||||
const nameEl = document.getElementById('chat-image-lightbox-name');
|
||||
|
||||
if (!lightbox || !imageEl || !imageUrl) {
|
||||
return;
|
||||
}
|
||||
|
||||
imageEl.src = imageUrl;
|
||||
imageEl.alt = imageName;
|
||||
|
||||
if (nameEl) {
|
||||
nameEl.textContent = imageName;
|
||||
}
|
||||
|
||||
lightbox.style.display = 'block';
|
||||
document.body.style.overflow = 'hidden';
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭聊天图片大图预览层。
|
||||
*/
|
||||
function closeChatImageLightbox(event = null) {
|
||||
// 如果是点击事件,且点击的目标不是背景或关闭按钮(比如点击了图片本身且没有阻止冒泡),则不关闭
|
||||
// 已经在 HTML 中对 img 做了 stopPropagation,此处 event.target !== event.currentTarget 仍是安全的
|
||||
if (event && event.target !== event.currentTarget) {
|
||||
return;
|
||||
}
|
||||
|
||||
const lightbox = document.getElementById('chat-image-lightbox');
|
||||
if (!lightbox) return;
|
||||
|
||||
lightbox.style.display = 'none';
|
||||
|
||||
const imageEl = document.getElementById('chat-image-lightbox-img');
|
||||
if (imageEl) {
|
||||
imageEl.src = '';
|
||||
}
|
||||
|
||||
document.body.style.overflow = '';
|
||||
}
|
||||
|
||||
window.handleChatImageSelected = handleChatImageSelected;
|
||||
window.openChatImageLightbox = openChatImageLightbox;
|
||||
window.closeChatImageLightbox = closeChatImageLightbox;
|
||||
syncChatComposerAfterResume();
|
||||
|
||||
if (_contentInput) {
|
||||
@@ -3620,6 +3735,7 @@
|
||||
// 清理包厢窗口
|
||||
const say2 = document.getElementById('chat-messages-container2');
|
||||
if (say2) say2.innerHTML = '';
|
||||
lastAutosaveNode = null;
|
||||
|
||||
// 将当前最大消息 ID 保存至本地,刷新时只显示大于此 ID 的历史记录
|
||||
localStorage.setItem(`local_clear_msg_id_${window.chatContext.roomId}`, _maxMsgId);
|
||||
@@ -3776,8 +3892,10 @@
|
||||
detailDiv.innerHTML =
|
||||
`<span style="color: green;">${escapeHtml(levelInfo + gainInfo)}</span><span class="msg-time">(${timeStr})</span>`;
|
||||
// 手动存点和自动存点共用同一条界面占位,避免包厢窗口堆积旧存点通知。
|
||||
container2.querySelectorAll('[data-autosave="1"]').forEach(el => el.remove());
|
||||
lastAutosaveNode?.remove();
|
||||
lastAutosaveNode = detailDiv;
|
||||
container2.appendChild(detailDiv);
|
||||
pruneMessageContainer(container2, PRIVATE_MESSAGE_NODE_LIMIT);
|
||||
if (autoScroll) container2.scrollTop = container2.scrollHeight;
|
||||
} else {
|
||||
return;
|
||||
|
||||
Reference in New Issue
Block a user