diff --git a/nginx.conf.example b/nginx.conf.example index 27e27d1..5810027 100644 --- a/nginx.conf.example +++ b/nginx.conf.example @@ -35,6 +35,41 @@ map $http_upgrade $connection_upgrade { # 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 反向代理(核心配置 - 必须添加!) # Laravel Reverb 监听在 127.0.0.1:8080 # 浏览器通过 /app 和 /apps 路径发起 WebSocket 连接 @@ -112,6 +147,26 @@ map $http_upgrade $connection_upgrade { # include /www/server/panel/vhost/rewrite/chat.ay.lc.conf; # #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 反向代理 ⚡⚡⚡ # location /app { # proxy_pass http://127.0.0.1:8080; diff --git a/public/.htaccess b/public/.htaccess index b574a59..c950ee7 100644 --- a/public/.htaccess +++ b/public/.htaccess @@ -23,3 +23,16 @@ RewriteCond %{REQUEST_FILENAME} !-f RewriteRule ^ index.php [L] + + + SetEnvIf Request_URI "^/build/assets/" vite_hashed_asset=1 + + + + # Vite 构建产物文件名带 hash,可以长期缓存;普通 public 资源保持默认缓存策略。 + Header set Cache-Control "public, max-age=31536000, immutable" env=vite_hashed_asset + + + + AddOutputFilterByType DEFLATE text/plain text/css text/javascript application/javascript application/json application/xml image/svg+xml + diff --git a/resources/views/chat/frame.blade.php b/resources/views/chat/frame.blade.php index e0fb7d7..c750ee1 100644 --- a/resources/views/chat/frame.blade.php +++ b/resources/views/chat/frame.blade.php @@ -214,6 +214,26 @@ @include('chat.partials.daily-sign-in-modal') {{-- ═══════════ 游戏面板(partials/games/ 子目录,各自独立,包含 CSS + HTML + JS) ═══════════ --}} + @include('chat.partials.games.baccarat-panel') @include('chat.partials.games.slot-machine') @include('chat.partials.games.mystery-box') diff --git a/resources/views/chat/partials/games/baccarat-panel.blade.php b/resources/views/chat/partials/games/baccarat-panel.blade.php index 382e8a5..b663a95 100644 --- a/resources/views/chat/partials/games/baccarat-panel.blade.php +++ b/resources/views/chat/partials/games/baccarat-panel.blade.php @@ -806,8 +806,8 @@ } }); - /** 页面加载时:检查是否有进行中的局,有则自动恢复面板 */ - document.addEventListener('DOMContentLoaded', async () => { + /** 页面空闲时:检查是否有进行中的局,有则自动恢复面板 */ + document.addEventListener('DOMContentLoaded', () => window.deferChatGameBootstrap(async () => { try { // 先加载历史趋势 const histRes = await fetch('/baccarat/history'); @@ -859,5 +859,5 @@ } catch (e) { console.warn('[百家乐] 初始化失败', e); } - }); + })); diff --git a/resources/views/chat/partials/games/fortune-panel.blade.php b/resources/views/chat/partials/games/fortune-panel.blade.php index 9d19804..f250e84 100644 --- a/resources/views/chat/partials/games/fortune-panel.blade.php +++ b/resources/views/chat/partials/games/fortune-panel.blade.php @@ -355,7 +355,7 @@ } /** 页面加载时:检查游戏是否开启,若开启则初始化面板数据 */ - document.addEventListener('DOMContentLoaded', async () => { + document.addEventListener('DOMContentLoaded', () => window.deferChatGameBootstrap(async () => { try { const res = await fetch('/fortune/today'); const data = await res.json(); @@ -374,5 +374,5 @@ } catch (e) { console.warn('[神秘占卜] 初始化失败', e); } - }); + })); diff --git a/resources/views/chat/partials/games/game-hall.blade.php b/resources/views/chat/partials/games/game-hall.blade.php index 7ec9ae8..407d475 100644 --- a/resources/views/chat/partials/games/game-hall.blade.php +++ b/resources/views/chat/partials/games/game-hall.blade.php @@ -93,6 +93,11 @@ * 游戏大厅模块(使用 IIFE 隔离全局作用域,防止 const 重复初始化导致脚本块失败) */ (function() { + /** 游戏大厅状态短缓存,避免用户频繁开关弹窗时重复打多个状态接口 */ + const GAME_HALL_CACHE_TTL = 15000; + let gameHallStatusCache = null; + let gameHallStatusCacheAt = 0; + /** 游戏大厅配置定义(ID → 展示配置) */ const GAME_HALL_GAMES = [{ id: 'baccarat', @@ -399,7 +404,29 @@ 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 ?? {}; try { const r = await fetch('/games/enabled', { @@ -428,14 +455,11 @@ }) ); - 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'; - } - }; + gameHallStatusCache = { enabledGames, statuses }; + gameHallStatusCacheAt = Date.now(); + + return gameHallStatusCache; + } /** * 关闭游戏大厅弹窗 diff --git a/resources/views/chat/partials/games/horse-race-panel.blade.php b/resources/views/chat/partials/games/horse-race-panel.blade.php index 0a23149..df3b31f 100644 --- a/resources/views/chat/partials/games/horse-race-panel.blade.php +++ b/resources/views/chat/partials/games/horse-race-panel.blade.php @@ -792,7 +792,7 @@ }); /** 页面加载时恢复进行中的场次 */ - document.addEventListener('DOMContentLoaded', async () => { + document.addEventListener('DOMContentLoaded', () => window.deferChatGameBootstrap(async () => { try { const panel = document.getElementById('horse-race-panel'); const histData = panel ? await Alpine.$data(panel).requestJson('/horse-race/history') : { history: [] }; @@ -846,5 +846,5 @@ } catch (e) { console.warn('[赛马] 初始化失败', e); } - }); + })); diff --git a/resources/views/chat/partials/scripts.blade.php b/resources/views/chat/partials/scripts.blade.php index 7a3a5de..2e9e6c1 100644 --- a/resources/views/chat/partials/scripts.blade.php +++ b/resources/views/chat/partials/scripts.blade.php @@ -73,6 +73,7 @@ let onlineUsers = {}; let autoScroll = true; let userBadgeRotationTick = 0; + let userListRenderTimer = null; let _maxMsgId = 0; // 记录当前收到的最大消息 ID const initialChatPreferences = normalizeChatPreferences(window.chatContext?.chatPreferences || {}); let blockedSystemSenders = new Set(initialChatPreferences.blocked_system_senders); @@ -1907,7 +1908,7 @@ */ function _renderUserListToContainer(targetContainer, sortBy, keyword) { if (!targetContainer) return; - targetContainer.innerHTML = ''; + const fragment = document.createDocumentFragment(); // 在列表顶部添加"大家"条目(原版风格) let allDiv = document.createElement('div'); @@ -1916,7 +1917,7 @@ allDiv.onclick = () => { toUserSelect.value = '大家'; }; - targetContainer.appendChild(allDiv); + fragment.appendChild(allDiv); // ── AI 小助手(已在 onlineUsers 中动态维护,移除硬编码)── @@ -1994,15 +1995,24 @@ _tapTime = now; } }, { passive: false }); - targetContainer.appendChild(item); + fragment.appendChild(item); }); + targetContainer.replaceChildren(fragment); refreshRenderedUserBadges(targetContainer); } function renderUserList() { - userList.innerHTML = ''; - toUserSelect.innerHTML = ''; + if (userListRenderTimer) { + 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'); @@ -2023,9 +2033,10 @@ text = '🤖 AI小班长'; } option.textContent = text; - toUserSelect.appendChild(option); + selectFragment.appendChild(option); } } + toUserSelect.replaceChildren(selectFragment); const count = Object.keys(onlineUsers).length; onlineCount.innerText = count; @@ -2039,6 +2050,22 @@ 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()); syncDailyStatusUi(); - renderUserList(); + scheduleRenderUserList(0); }); // 监听机器人动态开关 @@ -2596,7 +2623,7 @@ delete onlineUsers['AI小班长']; window.chatContext.botUser = null; } - renderUserList(); + scheduleRenderUserList(); }); window.addEventListener('chat:user-status-updated', (e) => { @@ -2614,19 +2641,19 @@ syncDailyStatusUi(); } - renderUserList(); + scheduleRenderUserList(); }); window.addEventListener('chat:joining', (e) => { const user = e.detail; hydrateOnlineUserPayload(user.username, user); - renderUserList(); + scheduleRenderUserList(); }); window.addEventListener('chat:leaving', (e) => { const user = e.detail; delete onlineUsers[user.username]; - renderUserList(); + scheduleRenderUserList(); }); window.addEventListener('chat:message', (e) => {