Files
chatroom/resources/views/chat/frame.blade.php
lkddi 602dcd7cf1 feat: 神秘箱子系统完整实现 + 婚姻状态弹窗 + 工具栏优化
## 新功能
- 神秘箱子系统(MysteryBox)完整实现:
  - 新增 MysteryBox / MysteryBoxClaim 模型及迁移文件
  - DropMysteryBoxJob / ExpireMysteryBoxJob 队列作业
  - MysteryBoxController(/mystery-box/status + /mystery-box/claim)
  - 支持三种类型:普通箱(500~2000金)/ 稀有箱(5000~20000金)/ 黑化箱(陷阱扣200~1000金)
  - 调度器自动投放 + 管理员手动投放
  - CurrencySource 新增 MYSTERY_BOX / MYSTERY_BOX_TRAP 枚举

- 婚姻状态弹窗(工具栏「婚姻」按钮):
  - 工具栏「呼叫」改为「婚姻」,点击打开婚姻状态弹窗
  - 动态渲染三种状态:单身 / 求婚中 / 已婚
  - 被求婚方可直接「答应 / 婉拒」;已婚可申请离婚(含二次确认)

## 优化修复
- frame.blade.php:Alpine.js CDN 补加 defer,修复所有组件初始化报错
- scripts.blade.php:神秘箱子暗号主动拦截(不依赖轮询),领取成功后弹 chatDialog 展示结果,更新金币余额
- MysteryBoxController:claim() 时 change() 补传 room_id 记录来源房间
- 后台游戏管理页(game-configs):投放箱子按钮颜色修复;弹窗替换为 window.adminDialog
- admin/layouts:新增全局 adminDialog 弹窗组件(替代原生 alert/confirm)
- baccarat-panel:FAB 拖动重构为 Alpine.js baccaratFab() 组件,与 slotFab 一致
- GAMES_TODO.md:神秘箱子移入已完成区,补全修复记录
2026-03-03 19:29:43 +08:00

221 lines
11 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{{--
文件功能聊天室主界面框架frame 页面)
全屏沉浸式布局,不使用统一 layout
CSS 抽取到 /public/css/chat.css
JS 抽取到 chat.partials.scripts Blade 模板
@author ChatRoom Laravel
@version 1.0.0
--}}
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ $room->name ?? '聊天室' }} - 飘落流星</title>
<meta name="csrf-token" content="{{ csrf_token() }}">
@php
// 从 sysparam 读取权限等级配置
$levelWarn = (int) \App\Models\Sysparam::getValue('level_warn', '5');
$levelKick = (int) \App\Models\Sysparam::getValue('level_kick', '10');
$levelMute = (int) \App\Models\Sysparam::getValue('level_mute', '8');
$levelBan = (int) \App\Models\Sysparam::getValue('level_ban', '12');
$levelBanip = (int) \App\Models\Sysparam::getValue('level_banip', '14');
$levelFreeze = (int) \App\Models\Sysparam::getValue('level_freeze', '14');
$superLevel = (int) \App\Models\Sysparam::getValue('superlevel', '100');
$myLevel = Auth::user()->user_level;
@endphp
<script>
window.chatContext = {
roomId: {{ $room->id }},
userId: {{ $user->id }},
username: "{{ $user->username }}",
userSex: "{{ match ((int) $user->sex) {1 => '男',2 => '女',default => ''} }}",
userLevel: {{ $user->user_level }},
superLevel: {{ $superLevel }},
levelKick: {{ $levelKick }},
levelMute: {{ $levelMute }},
levelBan: {{ $levelBan }},
levelBanip: {{ $levelBanip }},
sendUrl: "{{ route('chat.send', $room->id) }}",
leaveUrl: "{{ route('chat.leave', $room->id) }}",
heartbeatUrl: "{{ route('chat.heartbeat', $room->id) }}",
fishCastUrl: "{{ route('fishing.cast', $room->id) }}",
fishReelUrl: "{{ route('fishing.reel', $room->id) }}",
chatBotUrl: "{{ route('chatbot.chat') }}",
chatBotClearUrl: "{{ route('chatbot.clear') }}",
chatBotEnabled: {{ \App\Models\Sysparam::getValue('chatbot_enabled', '0') === '1' ? 'true' : 'false' }},
hasPosition: {{ Auth::user()->activePosition || Auth::user()->user_level >= $superLevel ? 'true' : 'false' }},
myMaxReward: @php
if (Auth::id() === 1) {
// 超级管理员id=1无需职务直接拥有不限量奖励权
echo -1;
} else {
$pos = Auth::user()->activePosition?->position;
// -1 = 有权限但无上限null0 = 禁止,正整数 = 有具体上限
echo $pos ? ($pos->max_reward === null ? -1 : (int) $pos->max_reward) : 0;
}
@endphp,
appointPositionsUrl: "{{ route('chat.appoint.positions') }}",
appointUrl: "{{ route('chat.appoint.appoint') }}",
revokeUrl: "{{ route('chat.appoint.revoke') }}",
rewardUrl: "{{ route('command.reward') }}",
rewardQuotaUrl: "{{ route('command.reward_quota') }}",
userJjb: {{ (int) $user->jjb }}, // 当前用户金币(求婚前金额预检查用)
// ─── 婚姻系统 ──────────────────────────────
minWeddingCost: {{ (int) \App\Models\WeddingTier::where('is_active', true)->orderBy('amount')->value('amount') ?? 0 }},
marriage: {
proposeUrl: "{{ route('marriage.propose') }}",
statusUrl: "{{ route('marriage.status') }}",
targteStatusUrl: "/marriage/target",
myRingsUrl: "{{ route('marriage.rings') }}",
acceptUrl: (id) => `/marriage/${id}/accept`,
rejectUrl: (id) => `/marriage/${id}/reject`,
divorceUrl: (id) => `/marriage/${id}/divorce`,
confirmDivorceUrl: (id) => `/marriage/${id}/confirm-divorce`,
rejectDivorceUrl: (id) => `/marriage/${id}/reject-divorce`,
divorceConfigUrl: '/marriage/divorce-config',
weddingTiersUrl: "/wedding/tiers",
weddingSetupUrl: (id) => `/wedding/${id}/setup`,
claimEnvelopeUrl: (id, ceremonyId) => `/wedding/${id}/claim`,
envelopeStatusUrl: (id) => `/wedding/${id}/envelope-status`,
}
};
</script>
@vite(['resources/css/app.css', 'resources/js/app.js', 'resources/js/chat.js'])
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.14.8/dist/cdn.min.js"></script>
<link rel="stylesheet" href="/css/chat.css">
</head>
<body>
<div class="chat-layout">
{{-- ═══════════ 左侧主区域 ═══════════ --}}
<div class="chat-left">
{{-- 顶部标题栏 + 公告滚动条(独立文件维护) --}}
@include('chat.partials.header')
{{-- 消息窗格(双窗格,默认只显示 say1 --}}
<div class="message-panes" id="message-panes">
{{-- 主消息窗 --}}
<div class="message-pane say1" id="chat-messages-container">
<div class="msg-line">
<span style="color: #cc0000; font-weight: bold;">【公众窗口】</span>显示公众的发言!
<span class="msg-time">({{ now()->format('H:i:s') }})</span><br>
<span
style="color: #000099;">{{ $room->name }}{{ $room->description ?? '欢迎光临!畅所欲言,文明聊天。' }}</span>
</div>
</div>
{{-- 副消息窗(包厢窗) --}}
<div class="message-pane say2" id="chat-messages-container2">
<div class="msg-line">
<span style="color: #cc0000; font-weight: bold;">【包厢窗口】</span>显示包厢名单中聊友的发言!
<span class="msg-time">({{ now()->format('H:i:s') }})</span>
</div>
</div>
</div>
{{-- 底部输入工具栏(独立文件维护) --}}
@include('chat.partials.input-bar')
</div>
{{-- ═══════════ 竖向工具条(独立文件维护) ═══════════ --}}
@include('chat.partials.toolbar')
{{-- ═══════════ 右侧用户面板(独立文件维护) ═══════════ --}}
@include('chat.partials.right-panel')
</div>
{{-- ═══════════ 全局自定义弹窗(替代原生 alert/confirm全页面可用 ═══════════ --}}
@include('chat.partials.global-dialog')
@include('chat.partials.toast-notification')
{{-- ═══════════ 聊天室交互脚本(独立文件维护) ═══════════ --}}
@include('chat.partials.user-actions')
{{-- ═══════════ 婚姻系统弹窗组件 ═══════════ --}}
@include('chat.partials.marriage-modals')
{{-- ═══════════ 节日福利弹窗组件 ═══════════ --}}
@include('chat.partials.holiday-modal')
{{-- ═══════════ 百家乐游戏面板 ═══════════ --}}
@include('chat.partials.baccarat-panel')
{{-- ═══════════ 老虎机游戏面板 ═══════════ --}}
@include('chat.partials.slot-machine')
{{-- ═══════════ 神秘箱子游戏面板 ═══════════ --}}
@include('chat.partials.mystery-box')
{{-- 全屏特效系统:管理员烟花/下雨/雷电/下雪 --}}
<script src="/js/effects/effect-sounds.js"></script>
<script src="/js/effects/effect-manager.js"></script>
<script src="/js/effects/fireworks.js"></script>
<script src="/js/effects/rain.js"></script>
<script src="/js/effects/lightning.js"></script>
<script src="/js/effects/snow.js"></script>
@include('chat.partials.scripts')
{{-- 页面初始加载时,渲染自带的历史记录(解决入场欢迎语错过断网的问题) --}}
@if (!empty($historyMessages))
<script>
document.addEventListener('DOMContentLoaded', () => {
const historyMsgs = @json($historyMessages);
const clearId = parseInt(localStorage.getItem(`local_clear_msg_id_{{ $room->id }}`) || '0', 10);
if (historyMsgs && historyMsgs.length > 0) {
// 全局函数 appendMessage 在 scripts.blade.php 中定义
historyMsgs.forEach(msg => {
// 如果开启了本地清屏,之前的历史记录不再显示
if (msg.id > clearId) {
if (typeof window.appendMessage === 'function') {
window.appendMessage(msg);
}
}
});
}
});
</script>
@endif
@if (!empty($newbieEffect) || !empty($weekEffect))
<script>
/**
* 延迟1秒待页面完成初始化后自动播放进房附带的特效
* 优先级:如果有新人礼包特效,优先播放新人大礼包;如果没有,再播放周卡特效
*/
setTimeout(() => {
if (window.EffectManager) {
@if (!empty($newbieEffect))
window.EffectManager.play('{{ $newbieEffect }}');
@elseif (!empty($weekEffect))
window.EffectManager.play('{{ $weekEffect }}');
@endif
}
}, 1000);
</script>
@endif
{{-- 页面初始加载时,若存在挂起的求婚 / 离婚请求,则弹窗 --}}
@if (!empty($pendingProposal) || !empty($pendingDivorce))
<script>
document.addEventListener('DOMContentLoaded', () => {
// 等待短暂延迟以确保 Alpine 和 window.chatDialog 初始化完成
setTimeout(() => {
@if (!empty($pendingProposal))
window.dispatchEvent(new CustomEvent('chat:marriage-proposed', {
detail: @json($pendingProposal)
}));
@endif
@if (!empty($pendingDivorce))
window.dispatchEvent(new CustomEvent('chat:divorce-requested', {
detail: @json($pendingDivorce)
}));
@endif
}, 800);
});
</script>
@endif
</body>
</html>