优化聊天室首屏和在线名单性能
This commit is contained in:
@@ -35,6 +35,41 @@ map $http_upgrade $connection_upgrade {
|
|||||||
# try_files $uri $uri/ /index.php?$query_string;
|
# try_files $uri $uri/ /index.php?$query_string;
|
||||||
# }
|
# }
|
||||||
|
|
||||||
|
# ── Vite 静态资源缓存(文件名带 hash,可安全长期缓存)────────────
|
||||||
|
location ^~ /build/assets/ {
|
||||||
|
access_log off;
|
||||||
|
expires 1y;
|
||||||
|
add_header Cache-Control "public, max-age=31536000, immutable" always;
|
||||||
|
try_files $uri =404;
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── 常规静态资源缓存(不带 hash,保守缓存 7 天)───────────────
|
||||||
|
location ~* \.(?:css|js|mjs|png|jpg|jpeg|gif|webp|svg|ico|woff2?|ttf|eot)$ {
|
||||||
|
access_log off;
|
||||||
|
expires 7d;
|
||||||
|
add_header Cache-Control "public, max-age=604800" always;
|
||||||
|
try_files $uri =404;
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── 传输压缩:宝塔若已全局开启,可保持全局配置,不必重复添加 ──────
|
||||||
|
gzip on;
|
||||||
|
gzip_comp_level 5;
|
||||||
|
gzip_min_length 1024;
|
||||||
|
gzip_vary on;
|
||||||
|
gzip_types
|
||||||
|
text/plain
|
||||||
|
text/css
|
||||||
|
text/javascript
|
||||||
|
application/javascript
|
||||||
|
application/json
|
||||||
|
application/xml
|
||||||
|
image/svg+xml;
|
||||||
|
|
||||||
|
# 如果服务器已安装 ngx_brotli,可启用以下配置获得更高压缩率:
|
||||||
|
# brotli on;
|
||||||
|
# brotli_comp_level 5;
|
||||||
|
# brotli_types text/plain text/css text/javascript application/javascript application/json application/xml image/svg+xml;
|
||||||
|
|
||||||
# ⚡ WebSocket 反向代理(核心配置 - 必须添加!)
|
# ⚡ WebSocket 反向代理(核心配置 - 必须添加!)
|
||||||
# Laravel Reverb 监听在 127.0.0.1:8080
|
# Laravel Reverb 监听在 127.0.0.1:8080
|
||||||
# 浏览器通过 /app 和 /apps 路径发起 WebSocket 连接
|
# 浏览器通过 /app 和 /apps 路径发起 WebSocket 连接
|
||||||
@@ -112,6 +147,26 @@ map $http_upgrade $connection_upgrade {
|
|||||||
# include /www/server/panel/vhost/rewrite/chat.ay.lc.conf;
|
# include /www/server/panel/vhost/rewrite/chat.ay.lc.conf;
|
||||||
# #REWRITE-END
|
# #REWRITE-END
|
||||||
#
|
#
|
||||||
|
# location ^~ /build/assets/ {
|
||||||
|
# access_log off;
|
||||||
|
# expires 1y;
|
||||||
|
# add_header Cache-Control "public, max-age=31536000, immutable" always;
|
||||||
|
# try_files $uri =404;
|
||||||
|
# }
|
||||||
|
#
|
||||||
|
# location ~* \.(?:css|js|mjs|png|jpg|jpeg|gif|webp|svg|ico|woff2?|ttf|eot)$ {
|
||||||
|
# access_log off;
|
||||||
|
# expires 7d;
|
||||||
|
# add_header Cache-Control "public, max-age=604800" always;
|
||||||
|
# try_files $uri =404;
|
||||||
|
# }
|
||||||
|
#
|
||||||
|
# gzip on;
|
||||||
|
# gzip_comp_level 5;
|
||||||
|
# gzip_min_length 1024;
|
||||||
|
# gzip_vary on;
|
||||||
|
# gzip_types text/plain text/css text/javascript application/javascript application/json application/xml image/svg+xml;
|
||||||
|
#
|
||||||
# # ⚡⚡⚡ 在这里插入 WebSocket 反向代理 ⚡⚡⚡
|
# # ⚡⚡⚡ 在这里插入 WebSocket 反向代理 ⚡⚡⚡
|
||||||
# location /app {
|
# location /app {
|
||||||
# proxy_pass http://127.0.0.1:8080;
|
# proxy_pass http://127.0.0.1:8080;
|
||||||
|
|||||||
@@ -23,3 +23,16 @@
|
|||||||
RewriteCond %{REQUEST_FILENAME} !-f
|
RewriteCond %{REQUEST_FILENAME} !-f
|
||||||
RewriteRule ^ index.php [L]
|
RewriteRule ^ index.php [L]
|
||||||
</IfModule>
|
</IfModule>
|
||||||
|
|
||||||
|
<IfModule mod_setenvif.c>
|
||||||
|
SetEnvIf Request_URI "^/build/assets/" vite_hashed_asset=1
|
||||||
|
</IfModule>
|
||||||
|
|
||||||
|
<IfModule mod_headers.c>
|
||||||
|
# Vite 构建产物文件名带 hash,可以长期缓存;普通 public 资源保持默认缓存策略。
|
||||||
|
Header set Cache-Control "public, max-age=31536000, immutable" env=vite_hashed_asset
|
||||||
|
</IfModule>
|
||||||
|
|
||||||
|
<IfModule mod_deflate.c>
|
||||||
|
AddOutputFilterByType DEFLATE text/plain text/css text/javascript application/javascript application/json application/xml image/svg+xml
|
||||||
|
</IfModule>
|
||||||
|
|||||||
@@ -214,6 +214,26 @@
|
|||||||
@include('chat.partials.daily-sign-in-modal')
|
@include('chat.partials.daily-sign-in-modal')
|
||||||
|
|
||||||
{{-- ═══════════ 游戏面板(partials/games/ 子目录,各自独立,包含 CSS + HTML + JS) ═══════════ --}}
|
{{-- ═══════════ 游戏面板(partials/games/ 子目录,各自独立,包含 CSS + HTML + JS) ═══════════ --}}
|
||||||
|
<script>
|
||||||
|
/**
|
||||||
|
* 延迟执行非关键游戏初始化,避免首屏聊天室渲染时同时抢占网络和主线程。
|
||||||
|
*
|
||||||
|
* @param {Function} callback 初始化回调
|
||||||
|
* @param {number} timeout 最长等待时间
|
||||||
|
*/
|
||||||
|
window.deferChatGameBootstrap = function(callback, timeout = 2500) {
|
||||||
|
if (typeof callback !== 'function') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('requestIdleCallback' in window) {
|
||||||
|
window.requestIdleCallback(callback, { timeout });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.setTimeout(callback, Math.min(timeout, 1200));
|
||||||
|
};
|
||||||
|
</script>
|
||||||
@include('chat.partials.games.baccarat-panel')
|
@include('chat.partials.games.baccarat-panel')
|
||||||
@include('chat.partials.games.slot-machine')
|
@include('chat.partials.games.slot-machine')
|
||||||
@include('chat.partials.games.mystery-box')
|
@include('chat.partials.games.mystery-box')
|
||||||
|
|||||||
@@ -806,8 +806,8 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
/** 页面加载时:检查是否有进行中的局,有则自动恢复面板 */
|
/** 页面空闲时:检查是否有进行中的局,有则自动恢复面板 */
|
||||||
document.addEventListener('DOMContentLoaded', async () => {
|
document.addEventListener('DOMContentLoaded', () => window.deferChatGameBootstrap(async () => {
|
||||||
try {
|
try {
|
||||||
// 先加载历史趋势
|
// 先加载历史趋势
|
||||||
const histRes = await fetch('/baccarat/history');
|
const histRes = await fetch('/baccarat/history');
|
||||||
@@ -859,5 +859,5 @@
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn('[百家乐] 初始化失败', e);
|
console.warn('[百家乐] 初始化失败', e);
|
||||||
}
|
}
|
||||||
});
|
}));
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -355,7 +355,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** 页面加载时:检查游戏是否开启,若开启则初始化面板数据 */
|
/** 页面加载时:检查游戏是否开启,若开启则初始化面板数据 */
|
||||||
document.addEventListener('DOMContentLoaded', async () => {
|
document.addEventListener('DOMContentLoaded', () => window.deferChatGameBootstrap(async () => {
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/fortune/today');
|
const res = await fetch('/fortune/today');
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
@@ -374,5 +374,5 @@
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn('[神秘占卜] 初始化失败', e);
|
console.warn('[神秘占卜] 初始化失败', e);
|
||||||
}
|
}
|
||||||
});
|
}));
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -93,6 +93,11 @@
|
|||||||
* 游戏大厅模块(使用 IIFE 隔离全局作用域,防止 const 重复初始化导致脚本块失败)
|
* 游戏大厅模块(使用 IIFE 隔离全局作用域,防止 const 重复初始化导致脚本块失败)
|
||||||
*/
|
*/
|
||||||
(function() {
|
(function() {
|
||||||
|
/** 游戏大厅状态短缓存,避免用户频繁开关弹窗时重复打多个状态接口 */
|
||||||
|
const GAME_HALL_CACHE_TTL = 15000;
|
||||||
|
let gameHallStatusCache = null;
|
||||||
|
let gameHallStatusCacheAt = 0;
|
||||||
|
|
||||||
/** 游戏大厅配置定义(ID → 展示配置) */
|
/** 游戏大厅配置定义(ID → 展示配置) */
|
||||||
const GAME_HALL_GAMES = [{
|
const GAME_HALL_GAMES = [{
|
||||||
id: 'baccarat',
|
id: 'baccarat',
|
||||||
@@ -399,7 +404,29 @@
|
|||||||
jjbEl.textContent = Number(window.chatContext.userJjb).toLocaleString();
|
jjbEl.textContent = Number(window.chatContext.userJjb).toLocaleString();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 每次打开均实时拉取后台开关状态(避免页面不刷新时开关不同步)
|
const { enabledGames, statuses } = await loadGameHallStatus();
|
||||||
|
|
||||||
|
try {
|
||||||
|
renderGameCards(enabledGames, statuses);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[游戏大厅] 渲染游戏卡片失败:', err);
|
||||||
|
document.getElementById('game-hall-loading').style.display = 'none';
|
||||||
|
document.getElementById('game-hall-empty').style.display = 'block';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加载游戏大厅状态,短时间内复用结果以减少重复请求。
|
||||||
|
*
|
||||||
|
* @returns {Promise<{enabledGames:Array, statuses:Object}>}
|
||||||
|
*/
|
||||||
|
async function loadGameHallStatus() {
|
||||||
|
const now = Date.now();
|
||||||
|
if (gameHallStatusCache && now - gameHallStatusCacheAt < GAME_HALL_CACHE_TTL) {
|
||||||
|
return gameHallStatusCache;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 打开大厅时拉取后台开关状态;短缓存能兼顾配置同步和重复打开速度。
|
||||||
let enabledMap = window.GAME_ENABLED ?? {};
|
let enabledMap = window.GAME_ENABLED ?? {};
|
||||||
try {
|
try {
|
||||||
const r = await fetch('/games/enabled', {
|
const r = await fetch('/games/enabled', {
|
||||||
@@ -428,14 +455,11 @@
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
try {
|
gameHallStatusCache = { enabledGames, statuses };
|
||||||
renderGameCards(enabledGames, statuses);
|
gameHallStatusCacheAt = Date.now();
|
||||||
} catch (err) {
|
|
||||||
console.error('[游戏大厅] 渲染游戏卡片失败:', err);
|
return gameHallStatusCache;
|
||||||
document.getElementById('game-hall-loading').style.display = 'none';
|
}
|
||||||
document.getElementById('game-hall-empty').style.display = 'block';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 关闭游戏大厅弹窗
|
* 关闭游戏大厅弹窗
|
||||||
|
|||||||
@@ -792,7 +792,7 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
/** 页面加载时恢复进行中的场次 */
|
/** 页面加载时恢复进行中的场次 */
|
||||||
document.addEventListener('DOMContentLoaded', async () => {
|
document.addEventListener('DOMContentLoaded', () => window.deferChatGameBootstrap(async () => {
|
||||||
try {
|
try {
|
||||||
const panel = document.getElementById('horse-race-panel');
|
const panel = document.getElementById('horse-race-panel');
|
||||||
const histData = panel ? await Alpine.$data(panel).requestJson('/horse-race/history') : { history: [] };
|
const histData = panel ? await Alpine.$data(panel).requestJson('/horse-race/history') : { history: [] };
|
||||||
@@ -846,5 +846,5 @@
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn('[赛马] 初始化失败', e);
|
console.warn('[赛马] 初始化失败', e);
|
||||||
}
|
}
|
||||||
});
|
}));
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -73,6 +73,7 @@
|
|||||||
let onlineUsers = {};
|
let onlineUsers = {};
|
||||||
let autoScroll = true;
|
let autoScroll = true;
|
||||||
let userBadgeRotationTick = 0;
|
let userBadgeRotationTick = 0;
|
||||||
|
let userListRenderTimer = null;
|
||||||
let _maxMsgId = 0; // 记录当前收到的最大消息 ID
|
let _maxMsgId = 0; // 记录当前收到的最大消息 ID
|
||||||
const initialChatPreferences = normalizeChatPreferences(window.chatContext?.chatPreferences || {});
|
const initialChatPreferences = normalizeChatPreferences(window.chatContext?.chatPreferences || {});
|
||||||
let blockedSystemSenders = new Set(initialChatPreferences.blocked_system_senders);
|
let blockedSystemSenders = new Set(initialChatPreferences.blocked_system_senders);
|
||||||
@@ -1907,7 +1908,7 @@
|
|||||||
*/
|
*/
|
||||||
function _renderUserListToContainer(targetContainer, sortBy, keyword) {
|
function _renderUserListToContainer(targetContainer, sortBy, keyword) {
|
||||||
if (!targetContainer) return;
|
if (!targetContainer) return;
|
||||||
targetContainer.innerHTML = '';
|
const fragment = document.createDocumentFragment();
|
||||||
|
|
||||||
// 在列表顶部添加"大家"条目(原版风格)
|
// 在列表顶部添加"大家"条目(原版风格)
|
||||||
let allDiv = document.createElement('div');
|
let allDiv = document.createElement('div');
|
||||||
@@ -1916,7 +1917,7 @@
|
|||||||
allDiv.onclick = () => {
|
allDiv.onclick = () => {
|
||||||
toUserSelect.value = '大家';
|
toUserSelect.value = '大家';
|
||||||
};
|
};
|
||||||
targetContainer.appendChild(allDiv);
|
fragment.appendChild(allDiv);
|
||||||
|
|
||||||
// ── AI 小助手(已在 onlineUsers 中动态维护,移除硬编码)──
|
// ── AI 小助手(已在 onlineUsers 中动态维护,移除硬编码)──
|
||||||
|
|
||||||
@@ -1994,15 +1995,24 @@
|
|||||||
_tapTime = now;
|
_tapTime = now;
|
||||||
}
|
}
|
||||||
}, { passive: false });
|
}, { passive: false });
|
||||||
targetContainer.appendChild(item);
|
fragment.appendChild(item);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
targetContainer.replaceChildren(fragment);
|
||||||
refreshRenderedUserBadges(targetContainer);
|
refreshRenderedUserBadges(targetContainer);
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderUserList() {
|
function renderUserList() {
|
||||||
userList.innerHTML = '';
|
if (userListRenderTimer) {
|
||||||
toUserSelect.innerHTML = '<option value="大家">大家</option>';
|
window.clearTimeout(userListRenderTimer);
|
||||||
|
userListRenderTimer = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectFragment = document.createDocumentFragment();
|
||||||
|
const everyoneOption = document.createElement('option');
|
||||||
|
everyoneOption.value = '大家';
|
||||||
|
everyoneOption.textContent = '大家';
|
||||||
|
selectFragment.appendChild(everyoneOption);
|
||||||
|
|
||||||
// 获取排序方式和搜索词
|
// 获取排序方式和搜索词
|
||||||
const sortSelect = document.getElementById('user-sort-select');
|
const sortSelect = document.getElementById('user-sort-select');
|
||||||
@@ -2023,9 +2033,10 @@
|
|||||||
text = '🤖 AI小班长';
|
text = '🤖 AI小班长';
|
||||||
}
|
}
|
||||||
option.textContent = text;
|
option.textContent = text;
|
||||||
toUserSelect.appendChild(option);
|
selectFragment.appendChild(option);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
toUserSelect.replaceChildren(selectFragment);
|
||||||
|
|
||||||
const count = Object.keys(onlineUsers).length;
|
const count = Object.keys(onlineUsers).length;
|
||||||
onlineCount.innerText = count;
|
onlineCount.innerText = count;
|
||||||
@@ -2039,6 +2050,22 @@
|
|||||||
window.dispatchEvent(new Event('chatroom:users-updated'));
|
window.dispatchEvent(new Event('chatroom:users-updated'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 合并高频在线名单变动,避免 Presence 连续进出时重复重建名单 DOM。
|
||||||
|
*
|
||||||
|
* @param {number} delay 等待毫秒数
|
||||||
|
*/
|
||||||
|
function scheduleRenderUserList(delay = 120) {
|
||||||
|
if (userListRenderTimer) {
|
||||||
|
window.clearTimeout(userListRenderTimer);
|
||||||
|
}
|
||||||
|
|
||||||
|
userListRenderTimer = window.setTimeout(() => {
|
||||||
|
userListRenderTimer = null;
|
||||||
|
renderUserList();
|
||||||
|
}, delay);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取用户当前仍然有效的当日状态。
|
* 获取用户当前仍然有效的当日状态。
|
||||||
*
|
*
|
||||||
@@ -2581,7 +2608,7 @@
|
|||||||
|
|
||||||
setOnlineUserDailyStatus(window.chatContext.username, getCurrentUserDailyStatus());
|
setOnlineUserDailyStatus(window.chatContext.username, getCurrentUserDailyStatus());
|
||||||
syncDailyStatusUi();
|
syncDailyStatusUi();
|
||||||
renderUserList();
|
scheduleRenderUserList(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
// 监听机器人动态开关
|
// 监听机器人动态开关
|
||||||
@@ -2596,7 +2623,7 @@
|
|||||||
delete onlineUsers['AI小班长'];
|
delete onlineUsers['AI小班长'];
|
||||||
window.chatContext.botUser = null;
|
window.chatContext.botUser = null;
|
||||||
}
|
}
|
||||||
renderUserList();
|
scheduleRenderUserList();
|
||||||
});
|
});
|
||||||
|
|
||||||
window.addEventListener('chat:user-status-updated', (e) => {
|
window.addEventListener('chat:user-status-updated', (e) => {
|
||||||
@@ -2614,19 +2641,19 @@
|
|||||||
syncDailyStatusUi();
|
syncDailyStatusUi();
|
||||||
}
|
}
|
||||||
|
|
||||||
renderUserList();
|
scheduleRenderUserList();
|
||||||
});
|
});
|
||||||
|
|
||||||
window.addEventListener('chat:joining', (e) => {
|
window.addEventListener('chat:joining', (e) => {
|
||||||
const user = e.detail;
|
const user = e.detail;
|
||||||
hydrateOnlineUserPayload(user.username, user);
|
hydrateOnlineUserPayload(user.username, user);
|
||||||
renderUserList();
|
scheduleRenderUserList();
|
||||||
});
|
});
|
||||||
|
|
||||||
window.addEventListener('chat:leaving', (e) => {
|
window.addEventListener('chat:leaving', (e) => {
|
||||||
const user = e.detail;
|
const user = e.detail;
|
||||||
delete onlineUsers[user.username];
|
delete onlineUsers[user.username];
|
||||||
renderUserList();
|
scheduleRenderUserList();
|
||||||
});
|
});
|
||||||
|
|
||||||
window.addEventListener('chat:message', (e) => {
|
window.addEventListener('chat:message', (e) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user