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) => {