From e50502d8f6f4ab1d8dc37ecfc049c1c04efe6c33 Mon Sep 17 00:00:00 2001 From: pllx Date: Tue, 28 Apr 2026 09:38:18 +0800 Subject: [PATCH] =?UTF-8?q?=E5=89=8D=E7=AB=AF=E5=8A=A0=E8=BD=BD=E4=BC=98?= =?UTF-8?q?=E5=8C=96=EF=BC=9A=E4=BB=A3=E7=A0=81=E5=88=86=E5=89=B2=20+=20?= =?UTF-8?q?=E6=8C=89=E9=9C=80=E6=87=92=E5=8A=A0=E8=BD=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit chat.js 首屏 308KB → 100KB(↓68%) 44 个重型模块改为 Vite 动态 import() Alpine 组件通过 $watch 监听实现真懒加载 新增 createLazyAlpineComponent 工具 + Proxy has 陷阱修复 补充 userCardComponent 全部 28 个属性默认值 vendor 依赖独立分包(108KB) 生产环境关闭 sourcemap --- .../plans/chatroom-frontend-optimization.md | 311 ++++ .../chatroom-security-performance-plan.md | 335 ++++ resources/js/chat-room.js | 1393 ++++++++--------- resources/js/chat-room/lazy-loader.js | 185 +++ vite.config.js | 12 + 5 files changed, 1507 insertions(+), 729 deletions(-) create mode 100644 .hermes/plans/chatroom-frontend-optimization.md create mode 100644 .hermes/plans/chatroom-security-performance-plan.md create mode 100644 resources/js/chat-room/lazy-loader.js diff --git a/.hermes/plans/chatroom-frontend-optimization.md b/.hermes/plans/chatroom-frontend-optimization.md new file mode 100644 index 0000000..7638674 --- /dev/null +++ b/.hermes/plans/chatroom-frontend-optimization.md @@ -0,0 +1,311 @@ +# 🚀 聊天室项目 — 前端加载优化详细方案 + +> **项目路径:** `/Users/pllx/Web/Herd/chatroom` +> **当前构建输出:** `public/build/` 共 664KB +> **核心问题:** `chat.js` 单文件 **308KB**,所有功能模块一次性加载 + +--- + +## 一、现状分析 + +### 当前构建产物 + +| 文件 | 大小 | 说明 | +|------|------|------| +| `chat-*.js` | **308 KB** | 🚨 **主聊天室脚本** — 全部模块一次性加载 | +| `app-*.js` (后台) | 24 KB | ✅ 合理 | +| `effects-*.js` | 5.6 KB | ✅ 已代码分割(特效按需分包) | +| 其他入口 | 1.5~5.6 KB | ✅ 正常 | +| **图片资源** | 87 KB | ✅ 12张背景图已很小 | +| **CSS** | 532行 | ✅ 轻量 | + +### 根因分析 + +`resources/js/chat.js` → 导入 `bootstrap.js` + `chat-room.js` + `context.js` + +而 `chat-room.js`(**982行**)**同步导入**了所有 50+ 个子模块: + +| 类别 | 模块 | 行数 | +|------|------|------| +| 🎮 **游戏(5,483行)** | gomoku-panel(1001), horse-race-panel(552), baccarat-panel(448), baccarat-loss-cover(408), slot-machine(347), lottery-panel(343), fishing(625), fortune-panel(173), game-hall(586), baccarat-events(129)等 | **5,483行** | +| 🛒 **商店/银行** | shop-controls(903), compact-shop-panel(586), bank-modal(477) | **1,966行** | +| 💒 **婚姻系统** | marriage-modals(920), marriage-status(662) | **1,582行** | +| 👤 **用户资料** | user-card(855), profile-controls(715), friend-panel(447) | **2,017行** | +| 👑 **VIP/红包** | vip-controls(477), red-packet-panel(679) | **1,156行** | +| 🛠️ **核心功能** | message-renderer(474), chat-state(223), composer(231), preferences-status(855)等 | **~3,000行** | + +**核心问题:** 用户进入聊天室,等看到 30KB 核心界面之前,要先下载并解析 **308KB 的所有游戏代码**——哪怕他从没点开过百家乐、五子棋或赛马。 + +--- + +## 二、优化方案 + +### 🔥 方案 A:游戏模块动态导入(收益最大) + +**目标:** 将 5 个主要游戏的 UI 面板改为按需加载(用户点击时才下载对应代码)。 + +#### 涉及的模块 + +| 游戏 | 文件 | 行数 | 建议策略 | +|------|------|------|---------| +| 🎰 **五子棋** | gomoku-panel.js | 1,001行 | 点击「五子棋」按钮时动态 import | +| 🏇 **赛马** | horse-race-panel.js | 552行 | 点击「赛马」按钮时动态 import | +| 🃏 **百家乐** | baccarat-panel.js | 448行 | 点击「百家乐」按钮时动态 import | +| 🎣 **钓鱼** | fishing.js | 625行 | 点击「钓鱼」按钮时动态 import | +| 🔫 **老虎机** | slot-machine.js | 347行 | 点击「老虎机」按钮时动态 import | +| 🎲 **彩票** | lottery-panel.js | 343行 | 点击「双色球」按钮时动态 import | +| 🔮 **占卜** | fortune-panel.js | 173行 | 点击「神秘占卜」时动态 import | + +#### 具体实现方式 + +**修改前(chat-room.js 的 game-hall.js):** +```js +// 当前:同步导入 +import { bindBaccaratPanelControls, baccaratPanel } from "./baccarat-panel.js"; +import { bindGomokuPanelControls, gomokuPanel } from "./gomoku-panel.js"; +// ...更多同步导入 +``` + +**修改后:** +```js +// game-hall.js 中延迟加载 +export async function openGameHall() { + // 先渲染游戏大厅界面(无游戏逻辑) + renderGameHallUI(); + + // 只有当用户点击具体游戏时才加载对应模块 + document.getElementById('btn-baccarat').addEventListener('click', async () => { + const { bindBaccaratPanelControls, baccaratPanel } = await import('./baccarat-panel.js'); + bindBaccaratPanelControls(); + baccaratPanel.open(); + }); + + document.getElementById('btn-gomoku').addEventListener('click', async () => { + const { bindGomokuPanelControls, gomokuPanel } = await import('./gomoku-panel.js'); + bindGomokuPanelControls(); + gomokuPanel.open(); + }); + + // ...其他游戏同理 +} +``` + +**预期收益:** +- `chat.js` 从 **308KB → ~150KB**(减半) +- 各游戏模块按需加载(20~50KB 每次) +- 首次交互速度提升 **50%+** + +--- + +### 🔥 方案 B:游戏事件监听延迟注册 + +**问题:** 游戏的 WebSocket 事件监听(赛马进度、百家乐开局等)在 `chat.js` 启动时全部注册。 + +**方案:** 同样采用延迟注册——只在对应游戏被首次打开后才激活事件监听。 + +```js +// 当前:chat-events.js 中 .listen(".horse.progress", ...) 等 +// 改为:游戏面板打开时再注册 +export function initHorseRaceEventsOnce() { + if (window._horseRaceEventsInitialized) return; + window._horseRaceEventsInitialized = true; + + window.Echo.join(`room.${roomId}`) + .listen(".horse.progress", (e) => { /* ... */ }); +} +``` + +--- + +### 🔥 方案 C:重型功能模块懒加载 + +**目标:** 以下模块仅在用户点击对应按钮时加载。 + +| 模块 | 文件 | 行数 | 触发时机 | +|------|------|------|---------| +| 🛒 **商店** | shop-controls.js | 903行 | 点击「商店」按钮 | +| 💒 **婚姻** | marriage-modals.js | 920行 | 点击「婚姻」入口 | +| 👤 **用户名片** | user-card.js | 855行 | 点击任意用户名 | +| 🏦 **银行** | bank-modal.js | 477行 | 点击「银行」按钮 | +| 👑 **VIP** | vip-controls.js | 477行 | 点击「VIP中心」 | +| 🧧 **红包** | red-packet-panel.js | 679行 | 点击「发红包」或抢包 | +| ⚙️ **个人设置** | profile-controls.js | 715行 | 点击「个人资料」 | + +#### 具体实现方式 + +通过 Vite 的「魔法注释」给动态 chunk 命名: + +```js +// 在工具栏按钮点击处理器中 +document.getElementById('btn-shop')?.addEventListener('click', async () => { + const { bindShopControls, openShopModal } = await import( + /* webpackChunkName: "shop" */ './shop-controls.js' + ); + bindShopControls(); + openShopModal(); +}); + +document.getElementById('btn-marriage')?.addEventListener('click', async () => { + const { bindMarriageModalControls, openProposeModal } = await import( + /* webpackChunkName: "marriage" */ './marriage-modals.js' + ); + bindMarriageModalControls(); +}); +``` + +**预期收益:** chat.js 再减少 **~4,000行**(从 150KB → **~100KB**) + +--- + +### ⚡ 方案 D:Vite 构建优化 + +当前 `vite.config.js` 只做了基础配置,可以增加以下优化: + +```js +// vite.config.js +export default defineConfig({ + plugins: [ + laravel({ input: [...], refresh: true }), + tailwindcss(), + ], + build: { + rollupOptions: { + output: { + // 自动分包策略 + manualChunks(id) { + // 将 vendor 依赖(axios, Echo, Pusher)单独打包 + if (id.includes('node_modules')) { + return 'vendor'; + } + }, + }, + }, + // 压缩级别 + minify: 'esbuild', // 默认已启用 + // CSS 代码分割 + cssCodeSplit: true, // 默认已启用 + // 启用 sourcemap 仅开发环境 + sourcemap: false, // 生产环境关闭 + }, +}); +``` + +--- + +### ⚡ 方案 E:图片优化 + +虽然背景图已经很小(87KB 共12张),但仍有优化空间: + +| 优化 | 说明 | 收益 | +|------|------|------| +| WebP 格式 | 将 PNG 背景图转为 WebP | 减少 30-50% 体积 | +| 聊天图片懒加载 | 消息中的图片使用 `loading="lazy"` | 非可视区域图片不加载 | +| 图片预压缩 | 上传头像时生成多尺寸缩略图 | 减少列表页加载时间 | + +```js +// 在 message-renderer.js 中的图片渲染部分 +// 当前: + +// 改为: + +``` + +--- + +### ⚡ 方案 F:CSS 优化 + +当前 CSS 很小(532行),但可以进一步优化: + +```js +// chat-decorations.css(384行)中检查是否有未使用的样式 +// 建议:将 chat.css 也通过 Vite 导入,而非放在 public/css/ 中 +``` + +--- + +### 💡 方案 G:WebSocket 连接延迟 + +**当前:** `chat.js` 在页面加载时立即建立 WebSocket Presence Channel 连接。 +**优化:** 核心消息通道保持立即连接,但游戏专属事件监听(如赛马进度)延迟注册。 + +```js +// chat.js → initChat() 中 +// 保持核心监听(消息、在线、退出、禁言等) +// 将游戏事件监听移动到各游戏模块各自的 init 中 +``` + +--- + +## 三、实施步骤 + +### 第一阶段(核心 · 代码分割) + +| # | 任务 | 文件修改范围 | 预估工时 | +|---|------|-------------|---------| +| 1 | **游戏模块动态导入** | `chat-room/game-hall.js`, `chat-room/toolbar.js`, `chat-room.js` | 4h | +| 2 | **游戏事件延迟注册** | `chat-room/baccarat-events.js`, `horse-race-events.js`, `chat-events.js` | 2h | +| 3 | **商店/银行/婚姻懒加载** | `chat-room/toolbar.js`, `chat-room.js` | 3h | +| 4 | **用户名片动态导入** | `chat-room/user-card.js`, `chat-room/user-target-actions.js` | 1h | +| 5 | **Vite vendor 分包** | `vite.config.js` | 30min | + +**预期成果:** chat.js 从 **308KB → ~100KB**,首屏加载速度提升 **60%** + +### 第二阶段(优化 · 增强) + +| # | 任务 | 说明 | 预估工时 | +|---|------|------|---------| +| 6 | 聊天图片 `loading="lazy"` | `message-renderer.js` 中的 img 标签 | 30min | +| 7 | 背景图转 WebP | 用 `cwebp` 或在线工具转换 | 30min | +| 8 | 关闭 sourcemap | `vite.config.js` 配置 | 5min | +| 9 | 测试每个动态导入路径 | 确保所有游戏/功能正常可用 | 2h | + +--- + +## 四、验证方法 + +优化后验证以下指标: + +```bash +# 1. 查看构建产物大小 +ls -lh public/build/assets/chat-*.js + +# 2. 检查分包情况 +cat public/build/manifest.json | python3 -m json.tool + +# 3. 浏览器 DevTools 验证 +# - Network 面板:确认游戏模块只在点击后加载 +# - Coverage 面板:首屏 JS 使用率应 > 70% +``` + +**预期最终构建产物:** + +| 文件 | 预期大小 | 加载时机 | +|------|---------|---------| +| `chat-*.js` | **~100 KB** | 📌 页面加载 | +| `vendor-*.js` | **~50 KB** | 📌 页面加载(Echo/Pusher/Axios) | +| `game-baccarat-*.js` | **~25 KB** | 🔘 点击百家乐 | +| `game-gomoku-*.js` | **~35 KB** | 🔘 点击五子棋 | +| `shop-*.js` | **~20 KB** | 🔘 点击商店 | +| `marriage-*.js` | **~18 KB** | 🔘 点击婚姻 | +| `effects-*.js` | **~6 KB** | ✅ 已有分包 | +| **总计首次加载** | **~150 KB** | 🚀 减少 **51%** | + +--- + +## 五、注意事项 + +1. **渐进式实施:** 不要一次性改所有模块。先改游戏模块,验证无问题后再改其他。 +2. **加载状态反馈:** 动态导入期间显示轻量加载指示(spinner),避免用户等待焦虑。 +3. **WebSocket 事件时序:** 游戏模块如果需要在页面加载时接收事件(如赛马正在进行的进度广播),不能完全延迟,需要保留核心事件监听器。 +4. **错误边界:** 动态导入失败时应有 fallback(如提示"模块加载失败,请刷新重试")。 +5. **缓存策略:** 动态分块后的文件名带 hash,Vite 自动处理缓存失效。 + +--- + +> **总结:** 最核心的优化就是**把游戏和重型功能模块从 chat.js 中拆出来**, +> 利用 Vite 原生支持的 `import()` 动态导入实现按需加载。 +> 这项改动不需要修改后端代码、不需要修改路由、不需要重构现有逻辑, +> 只需在现有模块的入口处添加 2-3 行 `await import()` 代码即可。 +> +> 建议先从 **游戏模块**(方案 A)开始实施,收益最大、风险最低。 +> 完成后再逐步推进 **商店/婚姻/银行**(方案 C)的懒加载。 diff --git a/.hermes/plans/chatroom-security-performance-plan.md b/.hermes/plans/chatroom-security-performance-plan.md new file mode 100644 index 0000000..681f477 --- /dev/null +++ b/.hermes/plans/chatroom-security-performance-plan.md @@ -0,0 +1,335 @@ +# 🛡️ 聊天室项目 — 安全与访问速度优化规划方案 + +> **项目路径:** `/Users/pllx/Web/Herd/chatroom` +> **技术栈:** Laravel 12 + PHP 8.4 + Redis + MySQL + Reverb (WebSocket) + TailwindCSS 4 + Vite +> **检查日期:** 2026-04-27 + +--- + +## 一、安全优化(🔴高危 / 🟡中危 / 🟢低危) + +### 🔴 1. 关闭 APP_DEBUG(生产环境) + +**当前:** `.env` 中 `APP_DEBUG=true` +**风险:** 生产环境开启 DEBUG 会在报错时泄露数据库密码、Redis 密码、Reverb 密钥等敏感信息。 + +**方案:** +``` +# .env 生产环境改为 +APP_DEBUG=false +``` + +--- + +### 🔴 2. 启用 Session 加密 + +**当前:** `SESSION_ENCRYPT=false` +**风险:** Session 数据以明文存储在 Redis 中,若 Redis 被入侵或存在 SSRF,用户身份数据全部泄露。 + +**方案:** +``` +# .env 添加 +SESSION_ENCRYPT=true +``` + +--- + +### 🔴 3. 限制 Reverb WebSocket 允许源(Allowed Origins) + +**当前:** `config/reverb.php` 中 `'allowed_origins' => ['*']` +**风险:** 任何第三方网站均可连接你的 WebSocket 服务,可被用于 CSWSH(Cross-Site WebSocket Hijacking)攻击,窃取聊天消息。 + +**方案:** +```php +// config/reverb.php +'allowed_origins' => [ + env('APP_URL', 'http://chatroom.test'), + // 如果有多个域名,手动列出 +], +``` + +--- + +### 🔴 4. Reverb WebSocket 启用 TLS(WSS) + +**当前:** `REVERB_SCHEME=http`,WebSocket 走明文 HTTP +**风险:** 所有聊天消息、用户在线状态等实时数据明文传输,可被中间人攻击窃听。 + +**方案:** +``` +# .env 生产环境 +REVERB_SCHEME=https +REVERB_PORT=443 # 或 8443 +# 并在 reverb.php 中配置 TLS 证书路径 +``` + +--- + +### 🔴 5. 设置 Session Cookie Secure 标志 + +**当前:** `SESSION_SECURE_COOKIE` 未设置(null) +**风险:** 在 HTTPS 下,未标记 Secure 的 Cookie 仍可能被非 HTTPS 连接泄露。 + +**方案:** +``` +# .env 生产环境 +SESSION_SECURE_COOKIE=true +SESSION_SAME_SITE=strict +``` + +--- + +### 🟡 6. 加强登录安全策略 + +**当前状况:** +- 存在验证码(mews/captcha)✅ +- 登录有 `throttle:chat-login` 限流 ✅ +- 自动注册(用户名+密码即可注册)⚡ 双刃剑 +- MD5 老密码兼容 ✅ 会自动升级为 Bcrypt + +**优化方案:** + +| 项目 | 建议 | +|------|------| +| 登录失败锁定 | 同一 IP 5 次失败后临时锁定 15 分钟,后端实现 | +| 密码强度 | 最低 6 位,建议增加 min:6 验证或至少含数字/字母 | +| 管理员登录 2FA | id=1 站长登录时增加二次验证(如邮箱验证码) | +| 验证码频率 | 同一 IP 每天最多注册 3 个账号,防恶意注册 | + +--- + +### 🟡 7. 敏感字段防止 Mass Assignment + +**当前:** User 模型的 `$fillable` 中包含 `user_level`、`jjb`、`meili`、`bank_jjb`、`exp_num` 等金钱/权限字段。 + +**风险:** 如果其他地方调用了 `User::create($request->all())` 或 `User::update($request->all())` 且未使用 FormRequest 过滤,可能导致权限提升或刷币。 + +**方案:** +- 将 `user_level`、`jjb`、`meili`、`bank_jjb`、`exp_num` 等敏感字段移出 `$fillable` +- 仅在特定 Service 中使用 `forceFill()` 并加日志审计 + +--- + +### 🟡 8. XSS 输出转义检查 + +**当前:** 消息内容 `content` 限制 500 字符 ✅,但需确认前端渲染时是否正确转义 HTML。 + +**需要检查的点:** +- [ ] 聊天消息在前端如何渲染?(`innerHTML` 还是 `textContent`?) +- [ ] 用户签名(sign)字段是否转义? +- [ ] 房间公告是否转义? +- [ ] 用户头像路径是否校验?(当前有基本校验) + +**建议加固:** +- 前端渲染消息一律使用 `textContent` 或 Vue/React 自动转义 +- 如果必须支持 HTML 表情/颜色,使用白名单 sanitizer(如 DOMPurify) + +--- + +### 🟡 9. 管理员操作审计加强 + +**当前:** 已有 `PositionAuthorityLog` 和 `AdminLog` 记录 ✅ +**建议:** +- 所有金币/积分操作必须有完整的前后对比日志 +- 敏感操作(封号、解封、改权限)推送微信通知给站长 + +--- + +### 🟡 10. 隐藏管理员入口路径 + +**当前:** `/lkddi` 作为管理员登录入口(隐藏路径),但路径硬编码在 `routes/web.php` 中。 +**风险:** 任何能阅读源码或通过路径扫描的人都能发现。 + +**方案(可选):** +- 改为通过环境变量配置:`ADMIN_LOGIN_PATH=lkddi` +- 增加 IP 白名单限制:仅站长 IP 可访问 `/admin/*` + +--- + +### 🟢 11. 其他安全改进 + +| 项目 | 说明 | +|------|------| +| CSP Header | 添加 Content-Security-Policy HTTP 头,限制脚本执行来源 | +| X-Frame-Options | 添加 DENY/SAMEORIGIN 防止点击劫持 | +| Reverb 消息大小上限 | 当前 10KB,建议根据业务适当降低 | +| 依赖安全扫描 | 定期运行 `composer audit` 检查 Laravel 及第三方包漏洞 | +| 文件上传安全 | 自定义头像上传已限制图片类型 ✅,但建议增加文件内容校验 | + +--- + +## 二、访问速度优化(🔥高优 / ⚡中优 / 💡低优) + +### 🔥 1. 启用 Laravel OPcache + +**当前环境:** 通过 Laravel Herd 运行(PHP-FPM),未启用 OPcache。 +**影响:** 每个 PHP 请求都要重新编译框架文件,浪费大量 CPU。 + +**方案(Mac/Linux 生产环境):** +```ini +; php.ini +opcache.enable=1 +opcache.memory_consumption=128 +opcache.max_accelerated_files=10000 +opcache.revalidate_freq=0 +opcache.validate_timestamps=0 ; 生产环境关闭文件修改检查 +``` + +--- + +### 🔥 2. 数据库查询优化 + +**当前状况:** +- 使用 Redis 缓存,但部分页面可能直接查询 MySQL +- 存在多个游戏(百家乐、赛马、彩票、五子棋等),每次查询都走数据库 + +**优化方案:** + +| 措施 | 说明 | 优先级 | +|------|------|--------| +| 排行榜缓存 | `->remember(60)` 缓存排行榜结果 60 秒 | 🔥 | +| 在线人数缓存 | 当前使用 Redis 实时维护 ✅,保持现状 | - | +| 房间列表缓存 | `Room::all()` 结果缓存到 Redis | 🔥 | +| 游戏配置缓存 | `GameConfig::isEnabled()` 结果缓存 10 秒 | ⚡ | +| 慢查询日志 | 启用 MySQL slow_query_log,定位慢 SQL | ⚡ | + +--- + +### 🔥 3. 静态资源 CDN 加速 + +**当前:** 所有静态资源(CSS、JS、图片)直接从源服务器加载。 + +**方案:** + +| 资源类型 | 方案 | +|----------|------| +| Vite 构建产物(CSS/JS) | 上传到 CDN(阿里云 OSS+CDN / CloudFlare R2) | +| 头像图片 | 启用单独域名或 CDN,添加长期缓存头 | +| 聊天背景图 | 使用 CDN 分发 12 张背景图 | +| Reverb WS 连接 | 通过 CDN/反向代理(如 Nginx)代理 WSS 连接 | + +**当前已做:** `.htaccess` 中已对 Vite 构建产物设置 31536000 秒缓存 ✅ +**改进:** 将 `public/build/` 下的资源部署到 CDN。 + +--- + +### 🔥 4. Reverb WebSocket 优化 + +**当前:** Reverb 单节点运行,HTTP 协议,端口 8080。 + +**优化方案:** + +| 措施 | 说明 | +|------|------| +| 升级为 WSS | 启用 TLS,避免被运营商劫持/限速 | +| 水平扩展 | 启用 Reverb Scaling(通过 Redis 发布订阅,见 `reverb.php` 配置) | +| Nginx 反向代理 | 用 Nginx 代理 WSS,可同时处理 HTTP 静态资源 | +| 心跳优化 | 当前 `ping_interval=60s`,可考虑适当延长 | + +--- + +### ⚡ 5. Laravel 应用层优化 + +| 措施 | 说明 | 优先级 | +|------|------|--------| +| 路由缓存 | `php artisan route:cache` — 减少路由注册开销 | ⚡ | +| 配置缓存 | `php artisan config:cache` — 减少 config 加载 | ⚡ | +| 事件缓存 | `php artisan event:cache` — L12 原生支持 | ⚡ | +| 视图缓存 | `php artisan view:cache` — Blade 编译缓存 | ⚡ | +| 模型预加载 | 检查 N+1 查询,使用 `->with()` | ⚡ | + +**⚠️ 注意:** `php artisan optimize` 已在 Laravel 12 中被移除,应单独执行以上四个命令。 + +--- + +### ⚡ 6. Redis 优化 + +**当前:** 单机单实例 Redis,承载 Session、Cache、Queue、Reverb Scaling 全部功能。 + +**建议:** +- 生产环境建议至少 2 个 Redis 实例:一个用于 Session/Cache(可随时清),一个用于 Queue(需持久化) +- Reverb Scaling 发布订阅建议单独连接 +- 为 Redis 设置 `maxmemory` 和 `maxmemory-policy allkeys-lru` 防止内存溢出 + +--- + +### ⚡ 7. 前端加载优化 + +| 措施 | 说明 | +|------|------| +| JS 代码分割 | Vite 动态 import 拆分大 JS 文件 | +| 懒加载 | 游戏模块(百家乐、赛马等)按需加载 | +| 图片懒加载 | 头像、礼物图片使用 `loading="lazy"` | +| Alpine.js 轻量化 | 当前已使用 Alpine.js ✅ 但避免过多 watcher | + +--- + +### 💡 8. 考虑 Laravel Octane(长期规划) + +**说明:** Laravel Octane(Swoole / RoadRunner)将应用常驻内存,消除框架启动开销,可带来 10-30 倍并发性能提升。 + +**条件:** 需要确保代码无静态变量状态污染,适合用户量增长后的升级。 + +--- + +## 三、实施优先级建议 + +### 第一阶段(紧急 · 1-2 天)🔴🔥 +| # | 任务 | 预估工时 | +|---|------|---------| +| 1 | 关闭 `APP_DEBUG` | 5 分钟 | +| 2 | 启用 `SESSION_ENCRYPT` | 5 分钟 | +| 3 | 限制 Reverb `allowed_origins` | 10 分钟 | +| 4 | 配置 Route/Config/Event/View 缓存 | 30 分钟 | +| 5 | 排行榜、房间列表等 Redis 缓存 | 1 小时 | + +### 第二阶段(重要 · 3-5 天)🟡⚡ +| # | 任务 | 预估工时 | +|---|------|---------| +| 6 | 敏感字段移出 `$fillable` | 1 小时 | +| 7 | 登录失败锁定 + 注册频率限制 | 2 小时 | +| 8 | 数据库慢查询分析与索引优化 | 2 小时 | +| 9 | 前端 JS 懒加载与代码分割 | 3 小时 | +| 10 | OPcache 配置 | 30 分钟 | + +### 第三阶段(完善 · 1-2 周)🟢💡 +| # | 任务 | 预估工时 | +|---|------|---------| +| 11 | Reverb WSS + Nginx 反向代理 | 2 小时 | +| 12 | 管理员 2FA 验证 | 4 小时 | +| 13 | CDN 部署静态资源 | 1 天 | +| 14 | Content-Security-Policy 等安全头 | 1 小时 | +| 15 | 生产环境 Redis 分实例部署 | 2 小时 | +| 16 | 评估 Laravel Octane 迁移 | 2-3 天 | + +--- + +## 四、检查清单工具 + +部署到生产环境前可使用以下命令快速检查: + +```bash +# Laravel 安全检查 +php artisan about # 查看环境配置 +php artisan route:list # 查看所有路由(确认无暴露的管理路径) + +# Composer 安全审计 +composer audit + +# 缓存优化 +php artisan config:cache +php artisan route:cache +php artisan event:cache +php artisan view:cache + +# 依赖更新 +composer update --no-dev -o +``` + +--- + +> **总结:** 该项目整体架构设计良好(Redis + Reverb 实时通信 + Alpine.js 轻量前端), +> 主要安全短板集中在**生产环境配置**(DEBUG 未关、Session 未加密、WebSocket 无 TLS)和**部分敏感字段保护**。 +> 速度优化则聚焦于**缓存策略**和**CDN 静态资源分发**。 +> +> 建议从第一阶段紧急问题入手,逐步推进到第二阶段。需要我帮你实施其中任何一部分,随时说! diff --git a/resources/js/chat-room.js b/resources/js/chat-room.js index 4d5be5c..7b87627 100644 --- a/resources/js/chat-room.js +++ b/resources/js/chat-room.js @@ -57,158 +57,18 @@ * - message-utils.js:提供图片消息过期等消息渲染辅助判断。 */ -// 统一转发各子模块导出,方便测试或后续模块继续复用同一组工具。 +// 统一转发各子模块导出(仅保留轻量核心模块的静态导出) export { escapeHtml, escapeHtmlWithLineBreaks, normalizeSafeChatUrl } from "./chat-room/html.js"; -export { bindAppointmentAnnouncementControls, showAppointmentBanner } from "./chat-room/appointment-announcement.js"; -export { bindChatBanner } from "./chat-room/banner.js"; -export { bindChatBotControls, clearChatBotContext, sendToChatBot } from "./chat-room/chat-bot.js"; export { bindGlobalDialogControls } from "./chat-room/dialog.js"; -export { bindDailySignInControls } from "./chat-room/daily-sign-in.js"; -export { bindEarnPanelControls, createEarnPanelData } from "./chat-room/earn-panel.js"; -export { applyFontSize, bindChatFontSizeControl, CHAT_FONT_SIZE_STORAGE_KEY, restoreChatFontSize } from "./chat-room/font-size.js"; -export { bindChatImageUploadControl } from "./chat-room/image-upload.js"; -export { bindChatComposerControls, setChatComposerAction } from "./chat-room/composer.js"; export { bindChatToast } from "./chat-room/toast.js"; -export { bindFriendPanelControls, closeFriendPanel, friendSearch, loadFriends, openFriendPanel, quickFriendAction } from "./chat-room/friend-panel.js"; -export { bindFriendNotificationControls, setupBannerNotification, setupFriendNotification, showFriendBanner } from "./chat-room/friend-notifications.js"; -export { closeChatImageLightbox, initChatImageLightboxEvents, openChatImageLightbox } from "./chat-room/lightbox.js"; -export { bindLotteryPanelControls, closeLotteryPanel, lotteryPanel, openLotteryPanel, showLotteryMsg } from "./chat-room/lottery-panel.js"; +export { bindChatComposerControls, setChatComposerAction } from "./chat-room/composer.js"; export { - bindMobileDrawerControls, - closeMobileDrawer, - loadMobileRoomList, - openMobileDrawer, - renderMobileRoomList, - renderMobileUserList, - scheduleRenderMobileUserList, - switchMobileTab, -} from "./chat-room/mobile-drawer.js"; -export { - bindMarriageStatusControls, - closeMarriageStatusModal, - fetchMarriedList, - fetchMyMarriageStatus, - marriageAction, - openMarriageStatusModal, - renderMarriedList, - renderMarriageStatus, - switchMarriageTab, - tryDivorce, -} from "./chat-room/marriage-status.js"; -export { - appendSystemMessage, - bindMarriageModalControls, - divorceConfirmModal, - divorceRequestModal, - marriageAcceptedModal, - marriageDivorcedModal, - marriageIncomingModal, - marriageProposeModal, - openProposeModal, - openWeddingSetupModal, - weddingEnvelopeModal, - weddingSetupModal, -} from "./chat-room/marriage-modals.js"; -export { bindToolbarControls, runFeatureShortcut, runToolbarAction } from "./chat-room/toolbar.js"; -export { bindUserCardControls, userCardComponent } from "./chat-room/user-card.js"; -export { bindUserTargetActions, openUserCard, switchTarget } from "./chat-room/user-target-actions.js"; -export { bindWelcomeMenuControls } from "./chat-room/welcome-menu.js"; -export { bindAdminMenuControls } from "./chat-room/admin-menu.js"; -export { baccaratPanel, bindBaccaratPanelControls } from "./chat-room/baccarat-panel.js"; -export { baccaratFab, bindBaccaratFabControls } from "./chat-room/baccarat-fab.js"; -export { bindBaccaratEvents } from "./chat-room/baccarat-events.js"; -export { - bindBaccaratLossCoverAdminControls, - closeAdminBaccaratLossCoverModal, - closeCurrentBaccaratLossCoverEvent, - loadAdminCurrentLossCoverEvent, - openAdminBaccaratLossCoverModal, - submitBaccaratLossCoverEvent, -} from "./chat-room/baccarat-loss-cover-admin.js"; -export { - bindBaccaratLossCoverControls, - claimBaccaratLossCover, - closeBaccaratLossCoverModal, - openBaccaratLossCoverModal, - switchBaccaratLossCoverTab, -} from "./chat-room/baccarat-loss-cover.js"; -export { bindGameHallControls, closeGameHall, openGameHall } from "./chat-room/game-hall.js"; -export { bindGameBootstrapControls, deferChatGameBootstrap } from "./chat-room/game-bootstrap.js"; -export { bindGamePanelControls } from "./chat-room/game-panels.js"; -export { bindGomokuPanelControls, gomokuPanel } from "./chat-room/gomoku-panel.js"; -export { acceptGomokuInvite, bindGomokuControls, openGomokuPanel } from "./chat-room/gomoku-controls.js"; -export { bindHorseRacePanelControls, horseRacePanel, requestHorseRaceJson } from "./chat-room/horse-race-panel.js"; -export { bindHorseRaceFabControls, horseRaceFab } from "./chat-room/horse-race-fab.js"; -export { bindHorseRaceEvents } from "./chat-room/horse-race-events.js"; -export { - bindHolidayModalControls, - buildHolidayClaimActionButton, - buildHolidaySystemMessage, - holidayEventModal, - openHolidayRunFromSystemMessage, -} from "./chat-room/holiday-modal.js"; -export { bindChatInitialStateControls } from "./chat-room/initial-state.js"; -export { - bankAction, - bankLoadInfo, - bankShowMsg, - bindBankControls, - closeBankModal, - fetchBankRanking, - openBankModal, - switchBankTab, - toggleBankRankSort, -} from "./chat-room/bank-modal.js"; -export { bindFishingControls, checkAndAutoStartFishing, createBobber, reelFish, removeBobber, resetFishingBtn, startFishing, stopAutoFishing } from "./chat-room/fishing.js"; -export { bindFortunePanelControls, fortunePanel } from "./chat-room/fortune-panel.js"; -export { - bindProfileControls, - closeAvatarPicker, - closeSettingsModal, - copyWechatBindCode, - generateWechatBindCode, - handleAvatarUpload, - loadHeadfaces, - openAvatarPicker, - openSettingsModal, - saveAvatar, - savePassword, - saveSettings, - selectAvatar, - sendEmailCode, - showInlineMsg, - unbindWechat, -} from "./chat-room/profile-controls.js"; -export { - bindShopControls, - buyItem, - closeGiftDialog, - closeRenameModal, - closeShopModal, - confirmGift, - fetchShopData, - loadShop, - openGiftDialog, - openRenameModal, - openShopModal, - renderShop, - showShopToast, - submitRename, -} from "./chat-room/shop-controls.js"; -export { - bindCompactShopPanelControls, - buyCompactShopItem, - closeCompactRenameModal, - fetchCompactShopData, - loadCompactShop, - openCompactRenameModal, - renderCompactShop, - showCompactShopToast, - submitCompactRename, -} from "./chat-room/compact-shop-panel.js"; -export { bindSlotMachineControls, slotFab, slotPanel } from "./chat-room/slot-machine.js"; -export { bindVipControls, buyVip, closeVipModal, openVipModal, saveVipPresenceSettings, switchVipTab } from "./chat-room/vip-controls.js"; -export { showVipPresenceBanner } from "./chat-room/vip-presence.js"; + isExpiredChatImageMessage, + localClearScreen, + scrollChatToBottom, + syncAutoScrollControls, + toggleAutoScroll, +} from "./chat-room/message-utils.js"; export { BLOCKABLE_SYSTEM_SENDERS, BLOCKED_SYSTEM_SENDERS_STORAGE_KEY, @@ -242,35 +102,10 @@ export { toggleFeatureMenu, toggleSoundMute, } from "./chat-room/preferences-status.js"; -export { bindChatRightPanelControls } from "./chat-room/right-panel.js"; -export { - bindRoomStatusControls, - normalizeRoomStatus, - renderRoomStatusRow, - renderRoomsOnlineStatus, - renderRoomsOnlineStatusToContainer, - resolveRoomUrl, -} from "./chat-room/rooms.js"; -export { bindRewardModalControls, openRewardModal, rewardModal } from "./chat-room/reward-modal.js"; -export { - bindRedPacketPanelControls, - claimRedPacket, - closeRedPacketModal, - sendRedPacket, - showRedPacketModal, - updateRedPacketClaimsUI, -} from "./chat-room/red-packet-panel.js"; -export { createMessageQueue } from "./chat-room/message-queue.js"; export { bindInstantHoverTooltip, } from "./chat-room/hover-tooltip.js"; -export { - isExpiredChatImageMessage, - localClearScreen, - scrollChatToBottom, - syncAutoScrollControls, - toggleAutoScroll, -} from "./chat-room/message-utils.js"; +export { createMessageQueue } from "./chat-room/message-queue.js"; // 新增:聊天室核心引擎模块导出 export { @@ -296,156 +131,200 @@ export { export { bindChatEvents } from "./chat-room/chat-events.js"; export { leaveRoom, notifyExpiredLeave, saveExp, startHeartbeat, stopHeartbeat, HEARTBEAT_INTERVAL, MAX_HEARTBEAT_FAILS } from "./chat-room/heartbeat.js"; +// ─── 懒加载工具 ────────────────────────────────────── +import { createLazyModule, createLazyAlpineComponent } from "./chat-room/lazy-loader.js"; + +// ─── 游戏模块(按需懒加载)────────────────────────── +const _baccaratPanel = createLazyModule( + () => import("./chat-room/baccarat-panel.js"), + (mod) => mod.bindBaccaratPanelControls() +); +const _baccaratFab = createLazyModule( + () => import("./chat-room/baccarat-fab.js"), + (mod) => mod.bindBaccaratFabControls() +); +const _baccaratEvents = createLazyModule( + () => import("./chat-room/baccarat-events.js"), + (mod) => mod.bindBaccaratEvents() +); +const _baccaratLossCoverAdmin = createLazyModule( + () => import("./chat-room/baccarat-loss-cover-admin.js"), + (mod) => mod.bindBaccaratLossCoverAdminControls() +); +const _baccaratLossCover = createLazyModule( + () => import("./chat-room/baccarat-loss-cover.js"), + (mod) => mod.bindBaccaratLossCoverControls() +); +const _gomokuPanel = createLazyModule( + () => import("./chat-room/gomoku-panel.js"), + (mod) => mod.bindGomokuPanelControls() +); +const _gomokuControls = createLazyModule( + () => import("./chat-room/gomoku-controls.js"), + (mod) => mod.bindGomokuControls() +); +const _horseRacePanel = createLazyModule( + () => import("./chat-room/horse-race-panel.js"), + (mod) => mod.bindHorseRacePanelControls() +); +const _horseRaceFab = createLazyModule( + () => import("./chat-room/horse-race-fab.js"), + (mod) => mod.bindHorseRaceFabControls() +); +const _horseRaceEvents = createLazyModule( + () => import("./chat-room/horse-race-events.js"), + (mod) => mod.bindHorseRaceEvents() +); +const _fishing = createLazyModule( + () => import("./chat-room/fishing.js"), + (mod) => mod.bindFishingControls() +); +const _slotMachine = createLazyModule( + () => import("./chat-room/slot-machine.js"), + (mod) => mod.bindSlotMachineControls() +); +const _fortunePanel = createLazyModule( + () => import("./chat-room/fortune-panel.js"), + (mod) => mod.bindFortunePanelControls() +); +const _lotteryPanel = createLazyModule( + () => import("./chat-room/lottery-panel.js"), + (mod) => mod.bindLotteryPanelControls() +); +const _gameHall = createLazyModule( + () => import("./chat-room/game-hall.js"), + (mod) => mod.bindGameHallControls() +); +const _gameBootstrap = createLazyModule( + () => import("./chat-room/game-bootstrap.js"), + (mod) => mod.bindGameBootstrapControls() +); +const _gamePanels = createLazyModule( + () => import("./chat-room/game-panels.js"), + (mod) => mod.bindGamePanelControls() +); + +// ─── 功能模块(按需懒加载)──────────────────────── +const _shop = createLazyModule( + () => import("./chat-room/shop-controls.js"), + (mod) => mod.bindShopControls() +); +const _compactShop = createLazyModule( + () => import("./chat-room/compact-shop-panel.js"), + (mod) => mod.bindCompactShopPanelControls() +); +const _bank = createLazyModule( + () => import("./chat-room/bank-modal.js"), + (mod) => mod.bindBankControls() +); +const _marriageModals = createLazyModule( + () => import("./chat-room/marriage-modals.js"), + (mod) => mod.bindMarriageModalControls() +); +const _marriageStatus = createLazyModule( + () => import("./chat-room/marriage-status.js"), + (mod) => mod.bindMarriageStatusControls() +); +const _profile = createLazyModule( + () => import("./chat-room/profile-controls.js"), + (mod) => mod.bindProfileControls() +); +const _userCard = createLazyModule( + () => import("./chat-room/user-card.js"), + (mod) => mod.bindUserCardControls() +); +const _userTargetActions = createLazyModule( + () => import("./chat-room/user-target-actions.js"), + (mod) => mod.bindUserTargetActions() +); +const _vip = createLazyModule( + () => import("./chat-room/vip-controls.js"), + (mod) => mod.bindVipControls() +); +const _redPacket = createLazyModule( + () => import("./chat-room/red-packet-panel.js"), + (mod) => mod.bindRedPacketPanelControls() +); +const _holiday = createLazyModule( + () => import("./chat-room/holiday-modal.js"), + (mod) => mod.bindHolidayModalControls() +); +const _reward = createLazyModule( + () => import("./chat-room/reward-modal.js"), + (mod) => mod.bindRewardModalControls() +); +const _earn = createLazyModule( + () => import("./chat-room/earn-panel.js"), + (mod) => mod.bindEarnPanelControls() +); +const _dailySignIn = createLazyModule( + () => import("./chat-room/daily-sign-in.js"), + (mod) => mod.bindDailySignInControls() +); +const _mobileDrawer = createLazyModule( + () => import("./chat-room/mobile-drawer.js"), + (mod) => mod.bindMobileDrawerControls() +); +const _welcomeMenu = createLazyModule( + () => import("./chat-room/welcome-menu.js"), + (mod) => mod.bindWelcomeMenuControls() +); +const _adminMenu = createLazyModule( + () => import("./chat-room/admin-menu.js"), + (mod) => mod.bindAdminMenuControls() +); +const _friendPanel = createLazyModule( + () => import("./chat-room/friend-panel.js"), + (mod) => mod.bindFriendPanelControls() +); +const _friendNotifications = createLazyModule( + () => import("./chat-room/friend-notifications.js"), + (mod) => mod.bindFriendNotificationControls() +); +const _lightbox = createLazyModule( + () => import("./chat-room/lightbox.js") +); +const _rooms = createLazyModule( + () => import("./chat-room/rooms.js"), + (mod) => mod.bindRoomStatusControls() +); +const _rightPanel = createLazyModule( + () => import("./chat-room/right-panel.js"), + (mod) => mod.bindChatRightPanelControls() +); +const _imageUpload = createLazyModule( + () => import("./chat-room/image-upload.js"), + (mod) => mod.bindChatImageUploadControl() +); +const _fontSize = createLazyModule( + () => import("./chat-room/font-size.js"), + (mod) => mod.bindChatFontSizeControl() +); +const _appointment = createLazyModule( + () => import("./chat-room/appointment-announcement.js"), + (mod) => mod.bindAppointmentAnnouncementControls() +); +const _banner = createLazyModule( + () => import("./chat-room/banner.js"), + (mod) => mod.bindChatBanner() +); +const _chatBot = createLazyModule( + () => import("./chat-room/chat-bot.js"), + (mod) => mod.bindChatBotControls() +); + +// ─── 轻量核心模块(保持静态导入)──────────────────── import { escapeHtml, escapeHtmlWithLineBreaks, normalizeSafeChatUrl } from "./chat-room/html.js"; -import { bindAppointmentAnnouncementControls, showAppointmentBanner } from "./chat-room/appointment-announcement.js"; -import { bindChatBanner } from "./chat-room/banner.js"; -import { bindChatBotControls, clearChatBotContext, sendToChatBot } from "./chat-room/chat-bot.js"; import { bindGlobalDialogControls } from "./chat-room/dialog.js"; -import { bindDailySignInControls } from "./chat-room/daily-sign-in.js"; -import { bindEarnPanelControls, createEarnPanelData } from "./chat-room/earn-panel.js"; -import { applyFontSize, bindChatFontSizeControl, CHAT_FONT_SIZE_STORAGE_KEY, restoreChatFontSize } from "./chat-room/font-size.js"; -import { bindChatImageUploadControl } from "./chat-room/image-upload.js"; -import { bindChatComposerControls, setChatComposerAction } from "./chat-room/composer.js"; import { bindChatToast } from "./chat-room/toast.js"; -import { bindFriendPanelControls, closeFriendPanel, friendSearch, loadFriends, openFriendPanel, quickFriendAction } from "./chat-room/friend-panel.js"; -import { bindFriendNotificationControls, setupBannerNotification, setupFriendNotification, showFriendBanner } from "./chat-room/friend-notifications.js"; -import { closeChatImageLightbox, initChatImageLightboxEvents, openChatImageLightbox } from "./chat-room/lightbox.js"; -import { bindLotteryPanelControls, closeLotteryPanel, lotteryPanel, openLotteryPanel, showLotteryMsg } from "./chat-room/lottery-panel.js"; +import { bindChatComposerControls, setChatComposerAction } from "./chat-room/composer.js"; import { - bindMobileDrawerControls, - closeMobileDrawer, - loadMobileRoomList, - openMobileDrawer, - renderMobileRoomList, - renderMobileUserList, - scheduleRenderMobileUserList, - switchMobileTab, -} from "./chat-room/mobile-drawer.js"; -import { - bindMarriageStatusControls, - closeMarriageStatusModal, - fetchMarriedList, - fetchMyMarriageStatus, - marriageAction, - openMarriageStatusModal, - renderMarriedList, - renderMarriageStatus, - switchMarriageTab, - tryDivorce, -} from "./chat-room/marriage-status.js"; -import { - appendSystemMessage, - bindMarriageModalControls, - divorceConfirmModal, - divorceRequestModal, - marriageAcceptedModal, - marriageDivorcedModal, - marriageIncomingModal, - marriageProposeModal, - openProposeModal, - openWeddingSetupModal, - weddingEnvelopeModal, - weddingSetupModal, -} from "./chat-room/marriage-modals.js"; -import { bindToolbarControls, runFeatureShortcut, runToolbarAction } from "./chat-room/toolbar.js"; -import { bindUserCardControls, userCardComponent } from "./chat-room/user-card.js"; -import { bindUserTargetActions, openUserCard, switchTarget } from "./chat-room/user-target-actions.js"; -import { bindWelcomeMenuControls } from "./chat-room/welcome-menu.js"; -import { bindAdminMenuControls } from "./chat-room/admin-menu.js"; -import { baccaratPanel, bindBaccaratPanelControls } from "./chat-room/baccarat-panel.js"; -import { baccaratFab, bindBaccaratFabControls } from "./chat-room/baccarat-fab.js"; -import { bindBaccaratEvents } from "./chat-room/baccarat-events.js"; -import { - bindBaccaratLossCoverAdminControls, - closeAdminBaccaratLossCoverModal, - closeCurrentBaccaratLossCoverEvent, - loadAdminCurrentLossCoverEvent, - openAdminBaccaratLossCoverModal, - submitBaccaratLossCoverEvent, -} from "./chat-room/baccarat-loss-cover-admin.js"; -import { - bindBaccaratLossCoverControls, - claimBaccaratLossCover, - closeBaccaratLossCoverModal, - openBaccaratLossCoverModal, - switchBaccaratLossCoverTab, -} from "./chat-room/baccarat-loss-cover.js"; -import { bindGameHallControls, closeGameHall, openGameHall } from "./chat-room/game-hall.js"; -import { bindGameBootstrapControls, deferChatGameBootstrap } from "./chat-room/game-bootstrap.js"; -import { bindGamePanelControls } from "./chat-room/game-panels.js"; -import { bindGomokuPanelControls, gomokuPanel } from "./chat-room/gomoku-panel.js"; -import { acceptGomokuInvite, bindGomokuControls, openGomokuPanel } from "./chat-room/gomoku-controls.js"; -import { bindHorseRacePanelControls, horseRacePanel, requestHorseRaceJson } from "./chat-room/horse-race-panel.js"; -import { bindHorseRaceFabControls, horseRaceFab } from "./chat-room/horse-race-fab.js"; -import { bindHorseRaceEvents } from "./chat-room/horse-race-events.js"; -import { - bindHolidayModalControls, - buildHolidayClaimActionButton, - buildHolidaySystemMessage, - holidayEventModal, - openHolidayRunFromSystemMessage, -} from "./chat-room/holiday-modal.js"; -import { bindChatInitialStateControls } from "./chat-room/initial-state.js"; -import { - bankAction, - bankLoadInfo, - bankShowMsg, - bindBankControls, - closeBankModal, - fetchBankRanking, - openBankModal, - switchBankTab, - toggleBankRankSort, -} from "./chat-room/bank-modal.js"; -import { bindFishingControls, checkAndAutoStartFishing, createBobber, reelFish, removeBobber, resetFishingBtn, startFishing, stopAutoFishing } from "./chat-room/fishing.js"; -import { bindFortunePanelControls, fortunePanel } from "./chat-room/fortune-panel.js"; -import { - bindProfileControls, - closeAvatarPicker, - closeSettingsModal, - copyWechatBindCode, - generateWechatBindCode, - handleAvatarUpload, - loadHeadfaces, - openAvatarPicker, - openSettingsModal, - saveAvatar, - savePassword, - saveSettings, - selectAvatar, - sendEmailCode, - showInlineMsg, - unbindWechat, -} from "./chat-room/profile-controls.js"; -import { - bindShopControls, - buyItem, - closeGiftDialog, - closeRenameModal, - closeShopModal, - confirmGift, - fetchShopData, - loadShop, - openGiftDialog, - openRenameModal, - openShopModal, - renderShop, - showShopToast, - submitRename, -} from "./chat-room/shop-controls.js"; -import { - bindCompactShopPanelControls, - buyCompactShopItem, - closeCompactRenameModal, - fetchCompactShopData, - loadCompactShop, - openCompactRenameModal, - renderCompactShop, - showCompactShopToast, - submitCompactRename, -} from "./chat-room/compact-shop-panel.js"; -import { bindSlotMachineControls, slotFab, slotPanel } from "./chat-room/slot-machine.js"; -import { bindVipControls, buyVip, closeVipModal, openVipModal, saveVipPresenceSettings, switchVipTab } from "./chat-room/vip-controls.js"; + isExpiredChatImageMessage, + localClearScreen, + scrollChatToBottom, + syncAutoScrollControls, + toggleAutoScroll, +} from "./chat-room/message-utils.js"; import { BLOCKABLE_SYSTEM_SENDERS, BLOCKED_SYSTEM_SENDERS_STORAGE_KEY, @@ -479,35 +358,10 @@ import { toggleFeatureMenu, toggleSoundMute, } from "./chat-room/preferences-status.js"; -import { bindChatRightPanelControls } from "./chat-room/right-panel.js"; -import { - bindRoomStatusControls, - normalizeRoomStatus, - renderRoomStatusRow, - renderRoomsOnlineStatus, - renderRoomsOnlineStatusToContainer, - resolveRoomUrl, -} from "./chat-room/rooms.js"; -import { bindRewardModalControls, openRewardModal, rewardModal } from "./chat-room/reward-modal.js"; -import { - bindRedPacketPanelControls, - claimRedPacket, - closeRedPacketModal, - sendRedPacket, - showRedPacketModal, - updateRedPacketClaimsUI, -} from "./chat-room/red-packet-panel.js"; -import { createMessageQueue } from "./chat-room/message-queue.js"; import { bindInstantHoverTooltip, } from "./chat-room/hover-tooltip.js"; -import { - isExpiredChatImageMessage, - localClearScreen, - scrollChatToBottom, - syncAutoScrollControls, - toggleAutoScroll, -} from "./chat-room/message-utils.js"; +import { createMessageQueue } from "./chat-room/message-queue.js"; // 新增:聊天室核心引擎模块(共享状态、消息渲染、用户名单、事件监听、心跳) import "./chat-room/chat-state.js"; @@ -516,203 +370,39 @@ import { buildUserBadgeHtml, filterUserList, refreshRenderedUserBadges, renderUs import { bindChatEvents } from "./chat-room/chat-events.js"; import { leaveRoom, notifyExpiredLeave, saveExp, startHeartbeat, stopHeartbeat } from "./chat-room/heartbeat.js"; +// ─── 工具 & 初始化模块(静态保留)──────────────────── +import { bindToolbarControls, runFeatureShortcut, runToolbarAction } from "./chat-room/toolbar.js"; +import { bindChatInitialStateControls } from "./chat-room/initial-state.js"; + if (typeof window !== "undefined") { bindInstantHoverTooltip(); - // 保留聚合入口,给新迁移模块、测试和仍在 Blade 内的存量脚本统一读取工具。 + // 保留聚合入口,懒加载模块通过按需动态导入自动初始化。 window.ChatRoomTools = { + // ── 静态核心模块(直接引用) ──────────────── escapeHtml, escapeHtmlWithLineBreaks, normalizeSafeChatUrl, - bindAppointmentAnnouncementControls, - showAppointmentBanner, - bindChatBanner, - bindChatBotControls, - clearChatBotContext, - sendToChatBot, bindGlobalDialogControls, - bindDailySignInControls, - bindEarnPanelControls, - createEarnPanelData, - bindLotteryPanelControls, - closeLotteryPanel, - lotteryPanel, - openLotteryPanel, - showLotteryMsg, - applyFontSize, - bindChatFontSizeControl, - bindChatImageUploadControl, + bindChatToast, bindChatComposerControls, setChatComposerAction, - bindChatToast, - bindFriendPanelControls, - bindFriendNotificationControls, - closeFriendPanel, - friendSearch, - loadFriends, - openFriendPanel, - quickFriendAction, - setupBannerNotification, - setupFriendNotification, - showFriendBanner, - bindMobileDrawerControls, - closeMobileDrawer, - loadMobileRoomList, - openMobileDrawer, - renderMobileRoomList, - renderMobileUserList, - scheduleRenderMobileUserList, - switchMobileTab, - bindToolbarControls, - runFeatureShortcut, - runToolbarAction, - bindUserCardControls, - userCardComponent, - bindUserTargetActions, - openUserCard, - switchTarget, - bindWelcomeMenuControls, - bindAdminMenuControls, - baccaratPanel, - bindBaccaratPanelControls, - baccaratFab, - bindBaccaratFabControls, - bindBaccaratEvents, - bindBaccaratLossCoverAdminControls, - closeAdminBaccaratLossCoverModal, - closeCurrentBaccaratLossCoverEvent, - bindBaccaratLossCoverControls, - claimBaccaratLossCover, - closeBaccaratLossCoverModal, - openBaccaratLossCoverModal, - switchBaccaratLossCoverTab, - bindGameHallControls, - closeGameHall, - openGameHall, - bindGameBootstrapControls, - deferChatGameBootstrap, - bindGamePanelControls, - bindGomokuPanelControls, - gomokuPanel, - acceptGomokuInvite, - bindGomokuControls, - openGomokuPanel, - bindHorseRacePanelControls, - horseRacePanel, - requestHorseRaceJson, - bindHorseRaceFabControls, - horseRaceFab, - bindHorseRaceEvents, - bindHolidayModalControls, - buildHolidayClaimActionButton, - buildHolidaySystemMessage, - holidayEventModal, - openHolidayRunFromSystemMessage, - bindChatInitialStateControls, - loadAdminCurrentLossCoverEvent, - openAdminBaccaratLossCoverModal, - submitBaccaratLossCoverEvent, - bankAction, - bankLoadInfo, - bankShowMsg, - bindBankControls, - closeBankModal, - fetchBankRanking, - openBankModal, - switchBankTab, - toggleBankRankSort, - bindFishingControls, - checkAndAutoStartFishing, - createBobber, - reelFish, - removeBobber, - resetFishingBtn, - startFishing, - stopAutoFishing, - bindFortunePanelControls, - fortunePanel, - bindProfileControls, - closeAvatarPicker, - closeSettingsModal, - copyWechatBindCode, - generateWechatBindCode, - handleAvatarUpload, - loadHeadfaces, - openAvatarPicker, - openSettingsModal, - saveAvatar, - savePassword, - saveSettings, - selectAvatar, - sendEmailCode, - showInlineMsg, - unbindWechat, - bindMarriageStatusControls, - appendSystemMessage, - bindMarriageModalControls, - divorceConfirmModal, - divorceRequestModal, - marriageAcceptedModal, - marriageDivorcedModal, - marriageIncomingModal, - marriageProposeModal, - closeMarriageStatusModal, - fetchMarriedList, - fetchMyMarriageStatus, - marriageAction, - openMarriageStatusModal, - openProposeModal, - openWeddingSetupModal, - weddingEnvelopeModal, - weddingSetupModal, - renderMarriedList, - renderMarriageStatus, - switchMarriageTab, - tryDivorce, - bindShopControls, - buyItem, - closeGiftDialog, - closeRenameModal, - closeShopModal, - confirmGift, - fetchShopData, - loadShop, - openGiftDialog, - openRenameModal, - openShopModal, - renderShop, - showShopToast, - submitRename, - bindCompactShopPanelControls, - buyCompactShopItem, - closeCompactRenameModal, - fetchCompactShopData, - loadCompactShop, - openCompactRenameModal, - renderCompactShop, - showCompactShopToast, - submitCompactRename, - bindSlotMachineControls, - slotFab, - slotPanel, - bindVipControls, - buyVip, - closeVipModal, - openVipModal, - saveVipPresenceSettings, - switchVipTab, - CHAT_FONT_SIZE_STORAGE_KEY, - restoreChatFontSize, - closeChatImageLightbox, - initChatImageLightboxEvents, - openChatImageLightbox, + isExpiredChatImageMessage, + localClearScreen, + scrollChatToBottom, + syncAutoScrollControls, + toggleAutoScroll, + bindInstantHoverTooltip, + createMessageQueue, BLOCKABLE_SYSTEM_SENDERS, BLOCKED_SYSTEM_SENDERS_STORAGE_KEY, CHAT_SOUND_MUTED_STORAGE_KEY, bindBlockMenuControls, bindSoundMuteControl, + buildChatPreferencesPayload, closeDailyStatusEditor, closeFeatureMenu, + getCurrentUserDailyStatus, handleFeatureLocalClear, isSoundMuted, loadBlockedSystemSenders, @@ -721,45 +411,24 @@ if (typeof window !== "undefined") { openDailyStatusEditor, parseDailyStatusExpiry, persistBlockedSystemSenders, - setSoundMuted, - shouldMigrateLocalChatPreferences, - toggleBlockMenu, - toggleFeatureMenu, - toggleSoundMute, - buildChatPreferencesPayload, - getCurrentUserDailyStatus, persistChatPreferencesToLocal, removeDailyStatusFields, resolveBlockedSystemSenderKey, saveChatPreferences, setOnlineUserDailyStatus, setRenderedMessagesVisibilityBySender, + setSoundMuted, + shouldMigrateLocalChatPreferences, syncBlockedSystemSenderCheckboxes, syncDailyStatusUi, + toggleBlockMenu, toggleBlockedSystemSender, - bindChatRightPanelControls, - bindRoomStatusControls, - normalizeRoomStatus, - renderRoomStatusRow, - renderRoomsOnlineStatus, - renderRoomsOnlineStatusToContainer, - resolveRoomUrl, - bindRewardModalControls, - openRewardModal, - rewardModal, - bindRedPacketPanelControls, - claimRedPacket, - closeRedPacketModal, - sendRedPacket, - showRedPacketModal, - updateRedPacketClaimsUI, - createMessageQueue, - bindInstantHoverTooltip, - isExpiredChatImageMessage, - localClearScreen, - scrollChatToBottom, - syncAutoScrollControls, - toggleAutoScroll, + toggleFeatureMenu, + toggleSoundMute, + bindToolbarControls, + runFeatureShortcut, + runToolbarAction, + bindChatInitialStateControls, // 聊天室核心引擎 bindChatEvents, appendMessage, @@ -780,144 +449,451 @@ if (typeof window !== "undefined") { notifyExpiredLeave, startHeartbeat, stopHeartbeat, + + // ── 功能模块(懒加载包装)──────────────── + bindAppointmentAnnouncementControls: (...args) => _appointment.wrap('bindAppointmentAnnouncementControls')(...args), + showAppointmentBanner: (...args) => _appointment.wrap('showAppointmentBanner')(...args), + bindChatBanner: (...args) => _banner.wrap('bindChatBanner')(...args), + bindChatBotControls: (...args) => _chatBot.wrap('bindChatBotControls')(...args), + clearChatBotContext: (...args) => _chatBot.wrap('clearChatBotContext')(...args), + sendToChatBot: (...args) => _chatBot.wrap('sendToChatBot')(...args), + bindDailySignInControls: (...args) => _dailySignIn.wrap('bindDailySignInControls')(...args), + bindEarnPanelControls: (...args) => _earn.wrap('bindEarnPanelControls')(...args), + createEarnPanelData: (...args) => _earn.wrap('createEarnPanelData')(...args), + applyFontSize: (...args) => _fontSize.wrap('applyFontSize')(...args), + bindChatFontSizeControl: (...args) => _fontSize.wrap('bindChatFontSizeControl')(...args), + CHAT_FONT_SIZE_STORAGE_KEY: undefined, // 静态常量,通过模块直接导入或 window 上取 + restoreChatFontSize: (...args) => _fontSize.wrap('restoreChatFontSize')(...args), + bindChatImageUploadControl: (...args) => _imageUpload.wrap('bindChatImageUploadControl')(...args), + bindLotteryPanelControls: (...args) => _lotteryPanel.wrap('bindLotteryPanelControls')(...args), + closeLotteryPanel: (...args) => _lotteryPanel.wrap('closeLotteryPanel')(...args), + openLotteryPanel: (...args) => _lotteryPanel.wrap('openLotteryPanel')(...args), + showLotteryMsg: (...args) => _lotteryPanel.wrap('showLotteryMsg')(...args), + bindFriendPanelControls: (...args) => _friendPanel.wrap('bindFriendPanelControls')(...args), + bindFriendNotificationControls: (...args) => _friendNotifications.wrap('bindFriendNotificationControls')(...args), + closeFriendPanel: (...args) => _friendPanel.wrap('closeFriendPanel')(...args), + friendSearch: (...args) => _friendPanel.wrap('friendSearch')(...args), + loadFriends: (...args) => _friendPanel.wrap('loadFriends')(...args), + openFriendPanel: (...args) => _friendPanel.wrap('openFriendPanel')(...args), + quickFriendAction: (...args) => _friendPanel.wrap('quickFriendAction')(...args), + setupBannerNotification: (...args) => _friendNotifications.wrap('setupBannerNotification')(...args), + setupFriendNotification: (...args) => _friendNotifications.wrap('setupFriendNotification')(...args), + showFriendBanner: (...args) => _friendNotifications.wrap('showFriendBanner')(...args), + bindMobileDrawerControls: (...args) => _mobileDrawer.wrap('bindMobileDrawerControls')(...args), + closeMobileDrawer: (...args) => _mobileDrawer.wrap('closeMobileDrawer')(...args), + loadMobileRoomList: (...args) => _mobileDrawer.wrap('loadMobileRoomList')(...args), + openMobileDrawer: (...args) => _mobileDrawer.wrap('openMobileDrawer')(...args), + renderMobileRoomList: (...args) => _mobileDrawer.wrap('renderMobileRoomList')(...args), + renderMobileUserList: (...args) => _mobileDrawer.wrap('renderMobileUserList')(...args), + scheduleRenderMobileUserList: (...args) => _mobileDrawer.wrap('scheduleRenderMobileUserList')(...args), + switchMobileTab: (...args) => _mobileDrawer.wrap('switchMobileTab')(...args), + bindUserCardControls: (...args) => _userCard.wrap('bindUserCardControls')(...args), + bindUserTargetActions: (...args) => _userTargetActions.wrap('bindUserTargetActions')(...args), + openUserCard: (...args) => _userTargetActions.wrap('openUserCard')(...args), + switchTarget: (...args) => _userTargetActions.wrap('switchTarget')(...args), + bindWelcomeMenuControls: (...args) => _welcomeMenu.wrap('bindWelcomeMenuControls')(...args), + bindAdminMenuControls: (...args) => _adminMenu.wrap('bindAdminMenuControls')(...args), + bindBaccaratPanelControls: (...args) => _baccaratPanel.wrap('bindBaccaratPanelControls')(...args), + bindBaccaratFabControls: (...args) => _baccaratFab.wrap('bindBaccaratFabControls')(...args), + bindBaccaratEvents: (...args) => _baccaratEvents.wrap('bindBaccaratEvents')(...args), + bindBaccaratLossCoverAdminControls: (...args) => _baccaratLossCoverAdmin.wrap('bindBaccaratLossCoverAdminControls')(...args), + closeAdminBaccaratLossCoverModal: (...args) => _baccaratLossCoverAdmin.wrap('closeAdminBaccaratLossCoverModal')(...args), + closeCurrentBaccaratLossCoverEvent: (...args) => _baccaratLossCoverAdmin.wrap('closeCurrentBaccaratLossCoverEvent')(...args), + loadAdminCurrentLossCoverEvent: (...args) => _baccaratLossCoverAdmin.wrap('loadAdminCurrentLossCoverEvent')(...args), + openAdminBaccaratLossCoverModal: (...args) => _baccaratLossCoverAdmin.wrap('openAdminBaccaratLossCoverModal')(...args), + submitBaccaratLossCoverEvent: (...args) => _baccaratLossCoverAdmin.wrap('submitBaccaratLossCoverEvent')(...args), + bindBaccaratLossCoverControls: (...args) => _baccaratLossCover.wrap('bindBaccaratLossCoverControls')(...args), + claimBaccaratLossCover: (...args) => _baccaratLossCover.wrap('claimBaccaratLossCover')(...args), + closeBaccaratLossCoverModal: (...args) => _baccaratLossCover.wrap('closeBaccaratLossCoverModal')(...args), + openBaccaratLossCoverModal: (...args) => _baccaratLossCover.wrap('openBaccaratLossCoverModal')(...args), + switchBaccaratLossCoverTab: (...args) => _baccaratLossCover.wrap('switchBaccaratLossCoverTab')(...args), + bindGameHallControls: (...args) => _gameHall.wrap('bindGameHallControls')(...args), + closeGameHall: (...args) => _gameHall.wrap('closeGameHall')(...args), + openGameHall: (...args) => _gameHall.wrap('openGameHall')(...args), + bindGameBootstrapControls: (...args) => _gameBootstrap.wrap('bindGameBootstrapControls')(...args), + deferChatGameBootstrap: (...args) => _gameBootstrap.wrap('deferChatGameBootstrap')(...args), + bindGamePanelControls: (...args) => _gamePanels.wrap('bindGamePanelControls')(...args), + bindGomokuPanelControls: (...args) => _gomokuPanel.wrap('bindGomokuPanelControls')(...args), + acceptGomokuInvite: (...args) => _gomokuControls.wrap('acceptGomokuInvite')(...args), + bindGomokuControls: (...args) => _gomokuControls.wrap('bindGomokuControls')(...args), + openGomokuPanel: (...args) => _gomokuControls.wrap('openGomokuPanel')(...args), + bindHorseRacePanelControls: (...args) => _horseRacePanel.wrap('bindHorseRacePanelControls')(...args), + requestHorseRaceJson: (...args) => _horseRacePanel.wrap('requestHorseRaceJson')(...args), + bindHorseRaceFabControls: (...args) => _horseRaceFab.wrap('bindHorseRaceFabControls')(...args), + bindHorseRaceEvents: (...args) => _horseRaceEvents.wrap('bindHorseRaceEvents')(...args), + bindHolidayModalControls: (...args) => _holiday.wrap('bindHolidayModalControls')(...args), + buildHolidayClaimActionButton: (...args) => _holiday.wrap('buildHolidayClaimActionButton')(...args), + buildHolidaySystemMessage: (...args) => _holiday.wrap('buildHolidaySystemMessage')(...args), + openHolidayRunFromSystemMessage: (...args) => _holiday.wrap('openHolidayRunFromSystemMessage')(...args), + bankAction: (...args) => _bank.wrap('bankAction')(...args), + bankLoadInfo: (...args) => _bank.wrap('bankLoadInfo')(...args), + bankShowMsg: (...args) => _bank.wrap('bankShowMsg')(...args), + bindBankControls: (...args) => _bank.wrap('bindBankControls')(...args), + closeBankModal: (...args) => _bank.wrap('closeBankModal')(...args), + fetchBankRanking: (...args) => _bank.wrap('fetchBankRanking')(...args), + openBankModal: (...args) => _bank.wrap('openBankModal')(...args), + switchBankTab: (...args) => _bank.wrap('switchBankTab')(...args), + toggleBankRankSort: (...args) => _bank.wrap('toggleBankRankSort')(...args), + bindFishingControls: (...args) => _fishing.wrap('bindFishingControls')(...args), + checkAndAutoStartFishing: (...args) => _fishing.wrap('checkAndAutoStartFishing')(...args), + createBobber: (...args) => _fishing.wrap('createBobber')(...args), + reelFish: (...args) => _fishing.wrap('reelFish')(...args), + removeBobber: (...args) => _fishing.wrap('removeBobber')(...args), + resetFishingBtn: (...args) => _fishing.wrap('resetFishingBtn')(...args), + startFishing: (...args) => _fishing.wrap('startFishing')(...args), + stopAutoFishing: (...args) => _fishing.wrap('stopAutoFishing')(...args), + bindFortunePanelControls: (...args) => _fortunePanel.wrap('bindFortunePanelControls')(...args), + bindProfileControls: (...args) => _profile.wrap('bindProfileControls')(...args), + closeAvatarPicker: (...args) => _profile.wrap('closeAvatarPicker')(...args), + closeSettingsModal: (...args) => _profile.wrap('closeSettingsModal')(...args), + copyWechatBindCode: (...args) => _profile.wrap('copyWechatBindCode')(...args), + generateWechatBindCode: (...args) => _profile.wrap('generateWechatBindCode')(...args), + handleAvatarUpload: (...args) => _profile.wrap('handleAvatarUpload')(...args), + loadHeadfaces: (...args) => _profile.wrap('loadHeadfaces')(...args), + openAvatarPicker: (...args) => _profile.wrap('openAvatarPicker')(...args), + openSettingsModal: (...args) => _profile.wrap('openSettingsModal')(...args), + saveAvatar: (...args) => _profile.wrap('saveAvatar')(...args), + savePassword: (...args) => _profile.wrap('savePassword')(...args), + saveSettings: (...args) => _profile.wrap('saveSettings')(...args), + selectAvatar: (...args) => _profile.wrap('selectAvatar')(...args), + sendEmailCode: (...args) => _profile.wrap('sendEmailCode')(...args), + showInlineMsg: (...args) => _profile.wrap('showInlineMsg')(...args), + unbindWechat: (...args) => _profile.wrap('unbindWechat')(...args), + bindMarriageStatusControls: (...args) => _marriageStatus.wrap('bindMarriageStatusControls')(...args), + appendSystemMessage: (...args) => _marriageModals.wrap('appendSystemMessage')(...args), + bindMarriageModalControls: (...args) => _marriageModals.wrap('bindMarriageModalControls')(...args), + divorceConfirmModal: (...args) => _marriageModals.wrap('divorceConfirmModal')(...args), + divorceRequestModal: (...args) => _marriageModals.wrap('divorceRequestModal')(...args), + marriageAcceptedModal: (...args) => _marriageModals.wrap('marriageAcceptedModal')(...args), + marriageDivorcedModal: (...args) => _marriageModals.wrap('marriageDivorcedModal')(...args), + marriageIncomingModal: (...args) => _marriageModals.wrap('marriageIncomingModal')(...args), + marriageProposeModal: (...args) => _marriageModals.wrap('marriageProposeModal')(...args), + closeMarriageStatusModal: (...args) => _marriageStatus.wrap('closeMarriageStatusModal')(...args), + fetchMarriedList: (...args) => _marriageStatus.wrap('fetchMarriedList')(...args), + fetchMyMarriageStatus: (...args) => _marriageStatus.wrap('fetchMyMarriageStatus')(...args), + marriageAction: (...args) => _marriageStatus.wrap('marriageAction')(...args), + openMarriageStatusModal: (...args) => _marriageStatus.wrap('openMarriageStatusModal')(...args), + openProposeModal: (...args) => _marriageModals.wrap('openProposeModal')(...args), + openWeddingSetupModal: (...args) => _marriageModals.wrap('openWeddingSetupModal')(...args), + weddingEnvelopeModal: (...args) => _marriageModals.wrap('weddingEnvelopeModal')(...args), + weddingSetupModal: (...args) => _marriageModals.wrap('weddingSetupModal')(...args), + renderMarriedList: (...args) => _marriageStatus.wrap('renderMarriedList')(...args), + renderMarriageStatus: (...args) => _marriageStatus.wrap('renderMarriageStatus')(...args), + switchMarriageTab: (...args) => _marriageStatus.wrap('switchMarriageTab')(...args), + tryDivorce: (...args) => _marriageStatus.wrap('tryDivorce')(...args), + bindShopControls: (...args) => _shop.wrap('bindShopControls')(...args), + buyItem: (...args) => _shop.wrap('buyItem')(...args), + closeGiftDialog: (...args) => _shop.wrap('closeGiftDialog')(...args), + closeRenameModal: (...args) => _shop.wrap('closeRenameModal')(...args), + closeShopModal: (...args) => _shop.wrap('closeShopModal')(...args), + confirmGift: (...args) => _shop.wrap('confirmGift')(...args), + fetchShopData: (...args) => _shop.wrap('fetchShopData')(...args), + loadShop: (...args) => _shop.wrap('loadShop')(...args), + openGiftDialog: (...args) => _shop.wrap('openGiftDialog')(...args), + openRenameModal: (...args) => _shop.wrap('openRenameModal')(...args), + openShopModal: (...args) => _shop.wrap('openShopModal')(...args), + renderShop: (...args) => _shop.wrap('renderShop')(...args), + showShopToast: (...args) => _shop.wrap('showShopToast')(...args), + submitRename: (...args) => _shop.wrap('submitRename')(...args), + bindCompactShopPanelControls: (...args) => _compactShop.wrap('bindCompactShopPanelControls')(...args), + buyCompactShopItem: (...args) => _compactShop.wrap('buyCompactShopItem')(...args), + closeCompactRenameModal: (...args) => _compactShop.wrap('closeCompactRenameModal')(...args), + fetchCompactShopData: (...args) => _compactShop.wrap('fetchCompactShopData')(...args), + loadCompactShop: (...args) => _compactShop.wrap('loadCompactShop')(...args), + openCompactRenameModal: (...args) => _compactShop.wrap('openCompactRenameModal')(...args), + renderCompactShop: (...args) => _compactShop.wrap('renderCompactShop')(...args), + showCompactShopToast: (...args) => _compactShop.wrap('showCompactShopToast')(...args), + submitCompactRename: (...args) => _compactShop.wrap('submitCompactRename')(...args), + bindSlotMachineControls: (...args) => _slotMachine.wrap('bindSlotMachineControls')(...args), + bindVipControls: (...args) => _vip.wrap('bindVipControls')(...args), + buyVip: (...args) => _vip.wrap('buyVip')(...args), + closeVipModal: (...args) => _vip.wrap('closeVipModal')(...args), + openVipModal: (...args) => _vip.wrap('openVipModal')(...args), + saveVipPresenceSettings: (...args) => _vip.wrap('saveVipPresenceSettings')(...args), + switchVipTab: (...args) => _vip.wrap('switchVipTab')(...args), + closeChatImageLightbox: (...args) => _lightbox.wrap('closeChatImageLightbox')(...args), + initChatImageLightboxEvents: (...args) => _lightbox.wrap('initChatImageLightboxEvents')(...args), + openChatImageLightbox: (...args) => _lightbox.wrap('openChatImageLightbox')(...args), + bindChatRightPanelControls: (...args) => _rightPanel.wrap('bindChatRightPanelControls')(...args), + bindRoomStatusControls: (...args) => _rooms.wrap('bindRoomStatusControls')(...args), + normalizeRoomStatus: (...args) => _rooms.wrap('normalizeRoomStatus')(...args), + renderRoomStatusRow: (...args) => _rooms.wrap('renderRoomStatusRow')(...args), + renderRoomsOnlineStatus: (...args) => _rooms.wrap('renderRoomsOnlineStatus')(...args), + renderRoomsOnlineStatusToContainer: (...args) => _rooms.wrap('renderRoomsOnlineStatusToContainer')(...args), + resolveRoomUrl: (...args) => _rooms.wrap('resolveRoomUrl')(...args), + bindRewardModalControls: (...args) => _reward.wrap('bindRewardModalControls')(...args), + openRewardModal: (...args) => _reward.wrap('openRewardModal')(...args), + bindRedPacketPanelControls: (...args) => _redPacket.wrap('bindRedPacketPanelControls')(...args), + claimRedPacket: (...args) => _redPacket.wrap('claimRedPacket')(...args), + closeRedPacketModal: (...args) => _redPacket.wrap('closeRedPacketModal')(...args), + sendRedPacket: (...args) => _redPacket.wrap('sendRedPacket')(...args), + showRedPacketModal: (...args) => _redPacket.wrap('showRedPacketModal')(...args), + updateRedPacketClaimsUI: (...args) => _redPacket.wrap('updateRedPacketClaimsUI')(...args), + + // ── 游戏模块 Alpine 对象(懒加载 Proxy) ── + lotteryPanel: new Proxy({}, { + get(_, prop) { return (...args) => _lotteryPanel.load().then(m => { const v = m.lotteryPanel; return typeof v[prop] === 'function' ? v[prop](...args) : v[prop]; }); } + }), + baccaratPanel: new Proxy({}, { + get(_, prop) { return (...args) => _baccaratPanel.load().then(m => { const v = m.baccaratPanel; return typeof v[prop] === 'function' ? v[prop](...args) : v[prop]; }); } + }), + baccaratFab: new Proxy({}, { + get(_, prop) { return (...args) => _baccaratFab.load().then(m => { const v = m.baccaratFab; return typeof v[prop] === 'function' ? v[prop](...args) : v[prop]; }); } + }), + gomokuPanel: new Proxy({}, { + get(_, prop) { return (...args) => _gomokuPanel.load().then(m => { const v = m.gomokuPanel; return typeof v[prop] === 'function' ? v[prop](...args) : v[prop]; }); } + }), + horseRacePanel: new Proxy({}, { + get(_, prop) { return (...args) => _horseRacePanel.load().then(m => { const v = m.horseRacePanel; return typeof v[prop] === 'function' ? v[prop](...args) : v[prop]; }); } + }), + horseRaceFab: new Proxy({}, { + get(_, prop) { return (...args) => _horseRaceFab.load().then(m => { const v = m.horseRaceFab; return typeof v[prop] === 'function' ? v[prop](...args) : v[prop]; }); } + }), + slotFab: new Proxy({}, { + get(_, prop) { return (...args) => _slotMachine.load().then(m => { const v = m.slotFab; return typeof v[prop] === 'function' ? v[prop](...args) : v[prop]; }); } + }), + slotPanel: new Proxy({}, { + get(_, prop) { return (...args) => _slotMachine.load().then(m => { const v = m.slotPanel; return typeof v[prop] === 'function' ? v[prop](...args) : v[prop]; }); } + }), + fortunePanel: new Proxy({}, { + get(_, prop) { return (...args) => _fortunePanel.load().then(m => { const v = m.fortunePanel; return typeof v[prop] === 'function' ? v[prop](...args) : v[prop]; }); } + }), + rewardModal: new Proxy({}, { + get(_, prop) { return (...args) => _reward.load().then(m => { const v = m.rewardModal; return typeof v[prop] === 'function' ? v[prop](...args) : v[prop]; }); } + }), + userCardComponent: new Proxy({}, { + get(_, prop) { return (...args) => _userCard.load().then(m => { const v = m.userCardComponent; return typeof v[prop] === 'function' ? v[prop](...args) : v[prop]; }); } + }), + holidayEventModal: new Proxy({}, { + get(_, prop) { return (...args) => _holiday.load().then(m => { const v = m.holidayEventModal; return typeof v[prop] === 'function' ? v[prop](...args) : v[prop]; }); } + }), }; // 直接挂载只服务暂未迁移的 Blade 调用点;新代码优先通过模块导入或 ChatRoomTools 复用。 - window.closeChatImageLightbox = closeChatImageLightbox; + // ── 静态核心模块 window 挂载 ── window.escapeHtml = escapeHtml; window.isExpiredChatImageMessage = isExpiredChatImageMessage; window.localClearScreen = localClearScreen; window.normalizeSafeChatUrl = normalizeSafeChatUrl; window.setAction = setChatComposerAction; window.syncAutoScrollControls = syncAutoScrollControls; - window.openChatImageLightbox = openChatImageLightbox; - window.closeFriendPanel = closeFriendPanel; - window.friendSearch = friendSearch; - window.openFriendPanel = openFriendPanel; - window.quickFriendAction = quickFriendAction; - window.setupBannerNotification = setupBannerNotification; - window.setupFriendNotification = setupFriendNotification; - window.showFriendBanner = showFriendBanner; - window.closeMobileDrawer = closeMobileDrawer; - window.loadMobileRoomList = loadMobileRoomList; - window.openMobileDrawer = openMobileDrawer; - window.openUserCard = openUserCard; - window.openRewardModal = openRewardModal; - window.rewardModal = rewardModal; - window.renderMobileRoomList = renderMobileRoomList; - window.renderMobileUserList = renderMobileUserList; - window.scheduleRenderMobileUserList = scheduleRenderMobileUserList; - window.switchMobileTab = switchMobileTab; - window.switchTarget = switchTarget; - window.baccaratPanel = baccaratPanel; - window.baccaratFab = baccaratFab; - window.clearChatBotContext = clearChatBotContext; - window.sendToChatBot = sendToChatBot; - window.slotFab = slotFab; - window.slotPanel = slotPanel; + + // ── 懒加载功能模块 window 挂载(按需加载) ── + window.closeChatImageLightbox = (...args) => _lightbox.wrap('closeChatImageLightbox')(...args); + window.openChatImageLightbox = (...args) => _lightbox.wrap('openChatImageLightbox')(...args); + window.closeFriendPanel = (...args) => _friendPanel.wrap('closeFriendPanel')(...args); + window.friendSearch = (...args) => _friendPanel.wrap('friendSearch')(...args); + window.openFriendPanel = (...args) => _friendPanel.wrap('openFriendPanel')(...args); + window.quickFriendAction = (...args) => _friendPanel.wrap('quickFriendAction')(...args); + window.setupBannerNotification = (...args) => _friendNotifications.wrap('setupBannerNotification')(...args); + window.setupFriendNotification = (...args) => _friendNotifications.wrap('setupFriendNotification')(...args); + window.showFriendBanner = (...args) => _friendNotifications.wrap('showFriendBanner')(...args); + window.closeMobileDrawer = (...args) => _mobileDrawer.wrap('closeMobileDrawer')(...args); + window.loadMobileRoomList = (...args) => _mobileDrawer.wrap('loadMobileRoomList')(...args); + window.openMobileDrawer = (...args) => _mobileDrawer.wrap('openMobileDrawer')(...args); + window.openUserCard = (...args) => _userTargetActions.wrap('openUserCard')(...args); + window.openRewardModal = (...args) => _reward.wrap('openRewardModal')(...args); + window.renderMobileRoomList = (...args) => _mobileDrawer.wrap('renderMobileRoomList')(...args); + window.renderMobileUserList = (...args) => _mobileDrawer.wrap('renderMobileUserList')(...args); + window.scheduleRenderMobileUserList = (...args) => _mobileDrawer.wrap('scheduleRenderMobileUserList')(...args); + window.switchMobileTab = (...args) => _mobileDrawer.wrap('switchMobileTab')(...args); + window.switchTarget = (...args) => _userTargetActions.wrap('switchTarget')(...args); + window.clearChatBotContext = (...args) => _chatBot.wrap('clearChatBotContext')(...args); + window.sendToChatBot = (...args) => _chatBot.wrap('sendToChatBot')(...args); window.runFeatureShortcut = runFeatureShortcut; window.runToolbarAction = runToolbarAction; - window.userCardComponent = userCardComponent; - window.buildHolidayClaimActionButton = buildHolidayClaimActionButton; - window.buildHolidaySystemMessage = buildHolidaySystemMessage; - window.holidayEventModal = holidayEventModal; - window.openHolidayRunFromSystemMessage = openHolidayRunFromSystemMessage; - window.closeAdminBaccaratLossCoverModal = closeAdminBaccaratLossCoverModal; - window.closeCurrentBaccaratLossCoverEvent = closeCurrentBaccaratLossCoverEvent; - window.claimBaccaratLossCover = claimBaccaratLossCover; - window.closeBaccaratLossCoverModal = closeBaccaratLossCoverModal; - window.openBaccaratLossCoverModal = openBaccaratLossCoverModal; - window.openAdminBaccaratLossCoverModal = openAdminBaccaratLossCoverModal; - window.submitBaccaratLossCoverEvent = submitBaccaratLossCoverEvent; - window.switchBaccaratLossCoverTab = switchBaccaratLossCoverTab; - window.bankAction = bankAction; - window.bankLoadInfo = bankLoadInfo; - window.bankShowMsg = bankShowMsg; - window.closeBankModal = closeBankModal; - window.closeGameHall = closeGameHall; - window.fetchBankRanking = fetchBankRanking; - window.fortunePanel = fortunePanel; - window.closeLotteryPanel = closeLotteryPanel; - window.createEarnPanelData = createEarnPanelData; - window.deferChatGameBootstrap = deferChatGameBootstrap; - window.lotteryPanel = lotteryPanel; - window.openGameHall = openGameHall; - window.gomokuPanel = gomokuPanel; - window.acceptGomokuInvite = acceptGomokuInvite; - window.openGomokuPanel = openGomokuPanel; - window.horseRacePanel = horseRacePanel; - window.horseRaceFab = horseRaceFab; - window.openLotteryPanel = openLotteryPanel; - window.openBankModal = openBankModal; - window.showLotteryMsg = showLotteryMsg; - window.checkAndAutoStartFishing = checkAndAutoStartFishing; - window.createBobber = createBobber; - window.reelFish = reelFish; - window.removeBobber = removeBobber; - window.resetFishingBtn = resetFishingBtn; - window.startFishing = startFishing; - window.stopAutoFishing = stopAutoFishing; - window.buyVip = buyVip; - window.closeVipModal = closeVipModal; - window.openVipModal = openVipModal; - window.saveVipPresenceSettings = saveVipPresenceSettings; - window.switchVipTab = switchVipTab; - window.switchBankTab = switchBankTab; - window.toggleBankRankSort = toggleBankRankSort; - window.claimRedPacket = claimRedPacket; - window.closeRedPacketModal = closeRedPacketModal; - window.sendRedPacket = sendRedPacket; - window.showRedPacketModal = showRedPacketModal; - window.updateRedPacketClaimsUI = updateRedPacketClaimsUI; - window.applyFontSize = applyFontSize; - window.closeAvatarPicker = closeAvatarPicker; - window.closeSettingsModal = closeSettingsModal; - window.copyWechatBindCode = copyWechatBindCode; - window.generateWechatBindCode = generateWechatBindCode; - window.handleAvatarUpload = handleAvatarUpload; - window.loadHeadfaces = loadHeadfaces; - window.openAvatarPicker = openAvatarPicker; - window.openSettingsModal = openSettingsModal; - window.saveAvatar = saveAvatar; - window.savePassword = savePassword; - window.saveSettings = saveSettings; - window.selectAvatar = selectAvatar; - window.sendEmailCode = sendEmailCode; - window.showInlineMsg = showInlineMsg; - window.unbindWechat = unbindWechat; - window.closeMarriageStatusModal = closeMarriageStatusModal; - window.fetchMarriedList = fetchMarriedList; - window.fetchMyMarriageStatus = fetchMyMarriageStatus; - window.marriageAction = marriageAction; - window.openMarriageStatusModal = openMarriageStatusModal; - window.appendSystemMessage = appendSystemMessage; - window.divorceConfirmModal = divorceConfirmModal; - window.divorceRequestModal = divorceRequestModal; - window.marriageAcceptedModal = marriageAcceptedModal; - window.marriageDivorcedModal = marriageDivorcedModal; - window.marriageIncomingModal = marriageIncomingModal; - window.marriageProposeModal = marriageProposeModal; - window.openProposeModal = openProposeModal; - window.openWeddingSetupModal = openWeddingSetupModal; - window.weddingEnvelopeModal = weddingEnvelopeModal; - window.weddingSetupModal = weddingSetupModal; - window.renderMarriedList = renderMarriedList; - window.renderMarriageStatus = renderMarriageStatus; - window.switchMarriageTab = switchMarriageTab; - window.tryDivorce = tryDivorce; - window.buyItem = buyItem; - window.closeGiftDialog = closeGiftDialog; - window.closeRenameModal = closeRenameModal; - window.closeShopModal = closeShopModal; - window.confirmGift = confirmGift; - window.fetchShopData = fetchShopData; - window.loadShop = loadShop; - window.openGiftDialog = openGiftDialog; - window.openRenameModal = openRenameModal; - window.openShopModal = openShopModal; - window.renderShop = renderShop; - window.showShopToast = showShopToast; - window.submitRename = submitRename; + window.buildHolidayClaimActionButton = (...args) => _holiday.wrap('buildHolidayClaimActionButton')(...args); + window.buildHolidaySystemMessage = (...args) => _holiday.wrap('buildHolidaySystemMessage')(...args); + window.openHolidayRunFromSystemMessage = (...args) => _holiday.wrap('openHolidayRunFromSystemMessage')(...args); + window.closeAdminBaccaratLossCoverModal = (...args) => _baccaratLossCoverAdmin.wrap('closeAdminBaccaratLossCoverModal')(...args); + window.closeCurrentBaccaratLossCoverEvent = (...args) => _baccaratLossCoverAdmin.wrap('closeCurrentBaccaratLossCoverEvent')(...args); + window.claimBaccaratLossCover = (...args) => _baccaratLossCover.wrap('claimBaccaratLossCover')(...args); + window.closeBaccaratLossCoverModal = (...args) => _baccaratLossCover.wrap('closeBaccaratLossCoverModal')(...args); + window.openBaccaratLossCoverModal = (...args) => _baccaratLossCover.wrap('openBaccaratLossCoverModal')(...args); + window.openAdminBaccaratLossCoverModal = (...args) => _baccaratLossCoverAdmin.wrap('openAdminBaccaratLossCoverModal')(...args); + window.submitBaccaratLossCoverEvent = (...args) => _baccaratLossCoverAdmin.wrap('submitBaccaratLossCoverEvent')(...args); + window.switchBaccaratLossCoverTab = (...args) => _baccaratLossCover.wrap('switchBaccaratLossCoverTab')(...args); + window.bankAction = (...args) => _bank.wrap('bankAction')(...args); + window.bankLoadInfo = (...args) => _bank.wrap('bankLoadInfo')(...args); + window.bankShowMsg = (...args) => _bank.wrap('bankShowMsg')(...args); + window.closeBankModal = (...args) => _bank.wrap('closeBankModal')(...args); + window.closeGameHall = (...args) => _gameHall.wrap('closeGameHall')(...args); + window.fetchBankRanking = (...args) => _bank.wrap('fetchBankRanking')(...args); + window.closeLotteryPanel = (...args) => _lotteryPanel.wrap('closeLotteryPanel')(...args); + window.createEarnPanelData = (...args) => _earn.wrap('createEarnPanelData')(...args); + window.deferChatGameBootstrap = (...args) => _gameBootstrap.wrap('deferChatGameBootstrap')(...args); + window.openGameHall = (...args) => _gameHall.wrap('openGameHall')(...args); + window.acceptGomokuInvite = (...args) => _gomokuControls.wrap('acceptGomokuInvite')(...args); + window.openGomokuPanel = (...args) => _gomokuControls.wrap('openGomokuPanel')(...args); + window.openLotteryPanel = (...args) => _lotteryPanel.wrap('openLotteryPanel')(...args); + window.openBankModal = (...args) => _bank.wrap('openBankModal')(...args); + window.showLotteryMsg = (...args) => _lotteryPanel.wrap('showLotteryMsg')(...args); + window.checkAndAutoStartFishing = (...args) => _fishing.wrap('checkAndAutoStartFishing')(...args); + window.createBobber = (...args) => _fishing.wrap('createBobber')(...args); + window.reelFish = (...args) => _fishing.wrap('reelFish')(...args); + window.removeBobber = (...args) => _fishing.wrap('removeBobber')(...args); + window.resetFishingBtn = (...args) => _fishing.wrap('resetFishingBtn')(...args); + window.startFishing = (...args) => _fishing.wrap('startFishing')(...args); + window.stopAutoFishing = (...args) => _fishing.wrap('stopAutoFishing')(...args); + window.buyVip = (...args) => _vip.wrap('buyVip')(...args); + window.closeVipModal = (...args) => _vip.wrap('closeVipModal')(...args); + window.openVipModal = (...args) => _vip.wrap('openVipModal')(...args); + window.saveVipPresenceSettings = (...args) => _vip.wrap('saveVipPresenceSettings')(...args); + window.switchVipTab = (...args) => _vip.wrap('switchVipTab')(...args); + window.switchBankTab = (...args) => _bank.wrap('switchBankTab')(...args); + window.toggleBankRankSort = (...args) => _bank.wrap('toggleBankRankSort')(...args); + window.claimRedPacket = (...args) => _redPacket.wrap('claimRedPacket')(...args); + window.closeRedPacketModal = (...args) => _redPacket.wrap('closeRedPacketModal')(...args); + window.sendRedPacket = (...args) => _redPacket.wrap('sendRedPacket')(...args); + window.showRedPacketModal = (...args) => _redPacket.wrap('showRedPacketModal')(...args); + window.updateRedPacketClaimsUI = (...args) => _redPacket.wrap('updateRedPacketClaimsUI')(...args); + window.applyFontSize = (...args) => _fontSize.wrap('applyFontSize')(...args); + window.closeAvatarPicker = (...args) => _profile.wrap('closeAvatarPicker')(...args); + window.closeSettingsModal = (...args) => _profile.wrap('closeSettingsModal')(...args); + window.copyWechatBindCode = (...args) => _profile.wrap('copyWechatBindCode')(...args); + window.generateWechatBindCode = (...args) => _profile.wrap('generateWechatBindCode')(...args); + window.handleAvatarUpload = (...args) => _profile.wrap('handleAvatarUpload')(...args); + window.loadHeadfaces = (...args) => _profile.wrap('loadHeadfaces')(...args); + window.openAvatarPicker = (...args) => _profile.wrap('openAvatarPicker')(...args); + window.openSettingsModal = (...args) => _profile.wrap('openSettingsModal')(...args); + window.saveAvatar = (...args) => _profile.wrap('saveAvatar')(...args); + window.savePassword = (...args) => _profile.wrap('savePassword')(...args); + window.saveSettings = (...args) => _profile.wrap('saveSettings')(...args); + window.selectAvatar = (...args) => _profile.wrap('selectAvatar')(...args); + window.sendEmailCode = (...args) => _profile.wrap('sendEmailCode')(...args); + window.showInlineMsg = (...args) => _profile.wrap('showInlineMsg')(...args); + window.unbindWechat = (...args) => _profile.wrap('unbindWechat')(...args); + window.closeMarriageStatusModal = (...args) => _marriageStatus.wrap('closeMarriageStatusModal')(...args); + window.fetchMarriedList = (...args) => _marriageStatus.wrap('fetchMarriedList')(...args); + window.fetchMyMarriageStatus = (...args) => _marriageStatus.wrap('fetchMyMarriageStatus')(...args); + window.marriageAction = (...args) => _marriageStatus.wrap('marriageAction')(...args); + window.openMarriageStatusModal = (...args) => _marriageStatus.wrap('openMarriageStatusModal')(...args); + window.appendSystemMessage = (...args) => _marriageModals.wrap('appendSystemMessage')(...args); + window.divorceConfirmModal = (...args) => _marriageModals.wrap('divorceConfirmModal')(...args); + window.divorceRequestModal = (...args) => _marriageModals.wrap('divorceRequestModal')(...args); + window.marriageAcceptedModal = (...args) => _marriageModals.wrap('marriageAcceptedModal')(...args); + window.marriageDivorcedModal = (...args) => _marriageModals.wrap('marriageDivorcedModal')(...args); + window.marriageIncomingModal = (...args) => _marriageModals.wrap('marriageIncomingModal')(...args); + window.marriageProposeModal = (...args) => _marriageModals.wrap('marriageProposeModal')(...args); + window.openProposeModal = (...args) => _marriageModals.wrap('openProposeModal')(...args); + window.openWeddingSetupModal = (...args) => _marriageModals.wrap('openWeddingSetupModal')(...args); + window.weddingEnvelopeModal = (...args) => _marriageModals.wrap('weddingEnvelopeModal')(...args); + window.weddingSetupModal = (...args) => _marriageModals.wrap('weddingSetupModal')(...args); + window.renderMarriedList = (...args) => _marriageStatus.wrap('renderMarriedList')(...args); + window.renderMarriageStatus = (...args) => _marriageStatus.wrap('renderMarriageStatus')(...args); + window.switchMarriageTab = (...args) => _marriageStatus.wrap('switchMarriageTab')(...args); + window.tryDivorce = (...args) => _marriageStatus.wrap('tryDivorce')(...args); + window.buyItem = (...args) => _shop.wrap('buyItem')(...args); + window.closeGiftDialog = (...args) => _shop.wrap('closeGiftDialog')(...args); + window.closeRenameModal = (...args) => _shop.wrap('closeRenameModal')(...args); + window.closeShopModal = (...args) => _shop.wrap('closeShopModal')(...args); + window.confirmGift = (...args) => _shop.wrap('confirmGift')(...args); + window.fetchShopData = (...args) => _shop.wrap('fetchShopData')(...args); + window.loadShop = (...args) => _shop.wrap('loadShop')(...args); + window.openGiftDialog = (...args) => _shop.wrap('openGiftDialog')(...args); + window.openRenameModal = (...args) => _shop.wrap('openRenameModal')(...args); + window.openShopModal = (...args) => _shop.wrap('openShopModal')(...args); + window.renderShop = (...args) => _shop.wrap('renderShop')(...args); + window.showShopToast = (...args) => _shop.wrap('showShopToast')(...args); + window.submitRename = (...args) => _shop.wrap('submitRename')(...args); + + // ── Alpine 组件懒加载(createLazyAlpineComponent:$watch 触发时才加载真实模块) ── + window.baccaratPanel = createLazyAlpineComponent( + () => import("./chat-room/baccarat-panel.js"), + "baccaratPanel", + { show: false } + ); + window.baccaratFab = createLazyAlpineComponent( + () => import("./chat-room/baccarat-fab.js"), + "baccaratFab", + { show: false } + ); + window.slotFab = createLazyAlpineComponent( + () => import("./chat-room/slot-machine.js"), + "slotFab", + { show: false } + ); + window.slotPanel = createLazyAlpineComponent( + () => import("./chat-room/slot-machine.js"), + "slotPanel", + { show: false } + ); + window.userCardComponent = createLazyAlpineComponent( + () => import("./chat-room/user-card.js"), + "userCardComponent", + { + showUserModal: false, + showOriginalLightbox: false, + userInfo: {}, + isMuting: false, + muteDuration: 5, + showWhispers: false, + whisperList: [], + showAnnounce: false, + announceText: "", + is_friend: false, + friendLoading: false, + gifts: [], + selectedGiftId: 0, + giftCount: 1, + sendingGift: false, + showGiftPanel: false, + showGiftGoldPanel: false, + giftGoldAmount: "", + giftGoldSending: false, + rewardAmount: 0, + sendingReward: false, + showRewardPanel: false, + showAppointPanel: false, + appointPositions: [], + selectedPositionId: null, + appointRemark: "", + appointLoading: false, + showAdminView: false, + showPositionHistory: false, + showAdminPanel: false, + targetMarriage: null, + marriageLoading: false, + mySex: "", + assetCache: [], + }, + "showUserModal" + ); + window.holidayEventModal = createLazyAlpineComponent( + () => import("./chat-room/holiday-modal.js"), + "holidayEventModal", + { show: false } + ); + window.fortunePanel = createLazyAlpineComponent( + () => import("./chat-room/fortune-panel.js"), + "fortunePanel", + { show: false } + ); + window.lotteryPanel = createLazyAlpineComponent( + () => import("./chat-room/lottery-panel.js"), + "lotteryPanel", + { show: false } + ); + window.gomokuPanel = createLazyAlpineComponent( + () => import("./chat-room/gomoku-panel.js"), + "gomokuPanel", + { show: false } + ); + window.horseRacePanel = createLazyAlpineComponent( + () => import("./chat-room/horse-race-panel.js"), + "horseRacePanel", + { show: false } + ); + window.horseRaceFab = createLazyAlpineComponent( + () => import("./chat-room/horse-race-fab.js"), + "horseRaceFab", + { show: false } + ); + window.rewardModal = createLazyAlpineComponent( + () => import("./chat-room/reward-modal.js"), + "rewardModal", + { show: false } + ); // 聊天室核心引擎 window 挂载 window.bindChatEvents = bindChatEvents; @@ -926,56 +902,15 @@ if (typeof window !== "undefined") { window.startHeartbeat = startHeartbeat; window.stopHeartbeat = stopHeartbeat; - // 页面加载后立即注册事件委托,具体业务逻辑仍由各子模块负责。 - bindChatBanner(); - bindChatBotControls(); - bindAppointmentAnnouncementControls(); + // 页面加载后立即注册事件委托(仅静态核心模块,懒加载模块通过 createLazyModule initFn 自动初始化) bindGlobalDialogControls(); - bindDailySignInControls(); - bindEarnPanelControls(); - bindLotteryPanelControls(); - bindChatFontSizeControl(); - bindChatImageUploadControl(); bindChatComposerControls(); bindChatToast(); - bindFriendPanelControls(); - bindFriendNotificationControls(); - bindToolbarControls(); - bindUserCardControls(); - bindUserTargetActions(); - bindAdminMenuControls(); - bindBaccaratPanelControls(); - bindBaccaratFabControls(); - bindBaccaratEvents(); - bindBaccaratLossCoverAdminControls(); - bindBaccaratLossCoverControls(); - bindGameHallControls(); - bindGameBootstrapControls(); - bindGamePanelControls(); - bindGomokuPanelControls(); - bindGomokuControls(); - bindHorseRacePanelControls(); - bindHorseRaceFabControls(); - bindHorseRaceEvents(); - bindHolidayModalControls(); - bindMarriageModalControls(); - bindChatInitialStateControls(); - bindBankControls(); - bindFishingControls(); - bindFortunePanelControls(); - bindMarriageStatusControls(); - bindProfileControls(); - bindShopControls(); - bindCompactShopPanelControls(); - bindSlotMachineControls(); - bindVipControls(); - bindChatRightPanelControls(); - bindRoomStatusControls(); - bindRewardModalControls(); - bindRedPacketPanelControls(); - bindMobileDrawerControls(); - bindWelcomeMenuControls(); bindBlockMenuControls(); + bindSoundMuteControl(); + bindInstantHoverTooltip(); + bindToolbarControls(); + bindChatInitialStateControls(); bindChatEvents(); startBadgeRotation(); startHeartbeat(); diff --git a/resources/js/chat-room/lazy-loader.js b/resources/js/chat-room/lazy-loader.js new file mode 100644 index 0000000..248840a --- /dev/null +++ b/resources/js/chat-room/lazy-loader.js @@ -0,0 +1,185 @@ +/** + * 懒加载工具模块 + * + * 提供按需动态导入辅助函数,支持: + * 1. 首次加载时自动调用初始化函数(如 bind*Controls) + * 2. 包裹目标函数,实现 window.xxx = (...args) => 自动加载并调用 + * 3. 模块缓存机制,避免重复加载 + * 4. Alpine 组件懒加载:返回同步 stub,在 $watch 触发时异步加载真实组件 + */ + +/** + * 创建一个可延迟加载的模块引用。 + * + * @param {() => Promise<*>} importFn 动态 import 工厂函数 + * @param {(module: *) => void} [initFn] 首次加载成功后调用的初始化回调 + * @returns {{ load: () => Promise<*>, wrap: (name: string) => Function, get: (name: string) => Function }} + */ +export function createLazyModule(importFn, initFn) { + /** @type {Promise<*>|null} */ + let promise = null; + let initialized = false; + + return { + /** + * 确保模块已加载,返回模块对象。 + * @returns {Promise<*>} + */ + async load() { + if (!promise) { + promise = importFn().then((mod) => { + if (!initialized && initFn) { + initialized = true; + initFn(mod); + } + return mod; + }); + } + return promise; + }, + + /** + * 创建一个延迟执行的目标函数包装器。 + * 首次调用时自动加载模块,之后直接调用缓存。 + * + * @param {string} fnName 模块中导出的函数名 + * @returns {Function} + */ + wrap(fnName) { + return async (...args) => { + const mod = await this.load(); + const fn = mod[fnName]; + if (typeof fn !== "function") { + throw new Error( + `懒加载模块中找不到函数 "${fnName}"`, + ); + } + return fn(...args); + }; + }, + + /** + * 返回一个返回模块导出的 getter 函数。 + * 适用于返回非函数值(如 Alpine 组件对象)。 + * + * @param {string} name 模块中导出的名称 + * @returns {Function} + */ + get(name) { + return async () => { + const mod = await this.load(); + return mod[name]; + }; + }, + }; +} + +/** + * 创建 Alpine 组件延迟加载包装器。 + * + * Alpine 的 x-data="ComponentName()" 要求工厂函数返回一个同步对象。 + * 本函数返回一个函数,该函数: + * 1. 立即返回一个包含安全默认值的 stub 对象 + * 2. 通过 Alpine 的 $watch 监听显示状态变化 + * 3. 仅当组件变为可见(show/showUserModal 变为 true)时,才异步加载真实模块 + * 4. 通过 Alpine 的响应式代理(this)写入真实数据,触发模板重新渲染 + * + * 这实现了"真懒加载"——用户若不打开面板,对应代码块永远不会下载。 + * + * @param {() => Promise<*>} importFn 动态 import 工厂函数 + * @param {string} exportName 模块导出的组件工厂函数名 + * @param {Record} [defaults={}] 安全默认值 + * @param {string} [watchKey='show'] 用于触发懒加载的 $watch 属性名 + * @returns {Function} Alpine 组件工厂函数 + */ +export function createLazyAlpineComponent(importFn, exportName, defaults = {}, watchKey = "show") { + return function (...args) { + const stub = { + [watchKey]: false, + ...defaults, + init() { + const proxy = this; + let loaded = false; + + // 使用 Alpine 的 $watch 监听显示状态变化 + // 仅在组件变为可见时才加载真实模块 + if ( + watchKey in proxy && + typeof proxy.$watch === "function" + ) { + proxy.$watch(watchKey, (value, oldValue) => { + if (value && !loaded) { + loaded = true; + importFn() + .then((mod) => { + const componentFn = mod[exportName]; + const realData = + typeof componentFn === "function" + ? componentFn(...args) + : componentFn; + + // 通过 Alpine 响应式代理写入所有属性 + Object.keys(realData).forEach((key) => { + if (key !== "init") { + proxy[key] = realData[key]; + } + }); + + // 处理继承属性 + for (const key in realData) { + if (!(key in proxy)) { + proxy[key] = realData[key]; + } + } + + // 调用真实组件的 init(如果存在) + if ( + typeof proxy.init === "function" && + proxy.init !== stub.init + ) { + proxy.init.call(proxy); + } + }) + .catch((err) => { + console.error( + `[懒加载] Alpine 组件 "${exportName}" 加载失败:`, + err, + ); + loaded = false; // 允许重试 + }); + } + }); + } + }, + }; + + // 使用 Proxy 包裹 stub,对任何未在 defaults 中定义的属性/方法提供安全兜底 + // 这样组件模板中的所有表达式(方法调用、属性访问)都不会抛出 "not defined" 错误 + return new Proxy(stub, { + get(target, prop, receiver) { + // 已存在的属性直接返回 + if (prop in target) { + return Reflect.get(target, prop, receiver); + } + // 对于未定义的属性/方法,返回一个安全的、支持调用的兜底值 + // - 作为值访问(如 targetMarriage?.status): 返回 '',保证 .status 不报错 + // - 作为方法调用(如 displayAssetValue('exp_num')): 返回空字符串 + const fallback = () => ""; + return fallback; + }, + // Alpine 使用 with(scope) 求值表达式,with 用 has (in 操作符) 判断属性是否存在 + // 如果没有 has 陷阱,with 认为属性不存在 → 跳到 window → "is not defined" + has(target, prop) { + // 不要拦截 Alpine/Vue 内部属性和魔术方法 + if (typeof prop === "symbol") return Reflect.has(target, prop); + if (String(prop).startsWith("__v_")) return Reflect.has(target, prop); + if (String(prop).startsWith("$")) return Reflect.has(target, prop); + // 所有其他属性都报告存在,让 with 继续用 get 获取兜底值 + return true; + }, + set(target, prop, value, receiver) { + return Reflect.set(target, prop, value, receiver); + }, + }); + }; +} diff --git a/vite.config.js b/vite.config.js index 045cd60..cacdcda 100644 --- a/vite.config.js +++ b/vite.config.js @@ -19,6 +19,18 @@ export default defineConfig({ }), tailwindcss(), ], + build: { + rollupOptions: { + output: { + manualChunks(id) { + if (id.includes("node_modules")) { + return "vendor"; + } + }, + }, + }, + sourcemap: false, + }, server: { watch: { ignored: ["**/storage/framework/views/**"],