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:神秘箱子移入已完成区,补全修复记录
This commit is contained in:
2026-03-03 19:29:43 +08:00
parent 40fcce2db3
commit 602dcd7cf1
21 changed files with 1799 additions and 139 deletions
@@ -295,130 +295,78 @@
</div>
</div>
{{-- ─── 悬浮入口(游戏开启时常驻,支持拖拽) ─── --}}
<div id="baccarat-fab" x-data="{ visible: false }" x-show="visible" x-cloak
style="position:fixed; bottom:90px; right:18px; z-index:9900; touch-action:none;">
<button id="baccarat-fab-btn" x-on:click.stop="() => {}"
style="width:52px; height:52px; border-radius:50%; border:none; cursor:grab;
{{-- ─── 骰悬浮入口(游戏开启时常驻,支持拖拽) ─── --}}
<div id="baccarat-fab" x-data="baccaratFab()" x-show="visible" x-cloak
:style="'position:fixed; right:' + posX + 'px; bottom:' + posY + 'px; z-index:9900; touch-action:none; user-select:none;'"
@pointerdown.prevent="startDrag($event)" @pointermove.window="onDrag($event)" @pointerup.window="endDrag($event)"
@pointercancel.window="endDrag($event)">
<button
style="width:52px; height:52px; border-radius:50%; border:none;
background:linear-gradient(135deg,#7c3aed,#4f46e5);
box-shadow:0 4px 20px rgba(124,58,237,.5);
font-size:22px; display:flex; align-items:center; justify-content:center;
animation:pulse-fab 2s infinite; user-select:none;"
title="百家乐下注中(可拖动)">🎲</button>
:style="dragging ? 'cursor:grabbing;' : 'cursor:grab;'" title="百家乐下注中(可拖动)">🎲</button>
</div>
<script>
/**
* 百家乐骰悬浮按钮拖拽逻辑
* 需在 DOM 就绪后初始化,因为 x-cloak 元素初始隐藏
* 百家乐骰悬浮按钮 Alpine 组件(拖动 + localStorage 位置持久化)
*/
(function initBaccaratFabDrag() {
const LS_KEY = 'baccaratFabPos';
function baccaratFab() {
const STORAGE_KEY = 'baccarat_fab_pos';
const saved = JSON.parse(localStorage.getItem(STORAGE_KEY) || 'null');
return {
visible: false,
posX: saved?.x ?? 18,
posY: saved?.y ?? 90,
dragging: false,
_startX: 0,
_startY: 0,
_origX: 0,
_origY: 0,
_moved: false,
function attachDrag() {
const fab = document.getElementById('baccarat-fab');
const btn = document.getElementById('baccarat-fab-btn');
if (!fab || !btn || fab._dragInited) return;
fab._dragInited = true;
startDrag(e) {
this.dragging = true;
this._moved = false;
this._startX = e.clientX;
this._startY = e.clientY;
this._origX = this.posX;
this._origY = this.posY;
e.currentTarget.setPointerCapture?.(e.pointerId);
},
// 恢复上次拖拽位置
const saved = (() => {
try {
return JSON.parse(localStorage.getItem(LS_KEY));
} catch {
return null;
}
})();
if (saved) {
fab.style.left = saved.left + 'px';
fab.style.top = saved.top + 'px';
fab.style.right = 'auto';
fab.style.bottom = 'auto';
}
onDrag(e) {
if (!this.dragging) return;
const dx = e.clientX - this._startX;
const dy = e.clientY - this._startY;
if (Math.abs(dx) > 3 || Math.abs(dy) > 3) this._moved = true;
this.posX = Math.max(4, Math.min(window.innerWidth - 60, this._origX - dx));
this.posY = Math.max(4, Math.min(window.innerHeight - 60, this._origY + dy));
},
let isDragging = false;
let startX, startY, startLeft, startTop;
endDrag(e) {
if (!this.dragging) return;
this.dragging = false;
localStorage.setItem(STORAGE_KEY, JSON.stringify({
x: this.posX,
y: this.posY
}));
if (!this._moved) this.openPanel();
},
function onStart(e) {
const rect = fab.getBoundingClientRect();
// 转为绝对 left/top
fab.style.left = rect.left + 'px';
fab.style.top = rect.top + 'px';
fab.style.right = 'auto';
fab.style.bottom = 'auto';
isDragging = false;
const cx = e.touches ? e.touches[0].clientX : e.clientX;
const cy = e.touches ? e.touches[0].clientY : e.clientY;
startX = cx;
startY = cy;
startLeft = rect.left;
startTop = rect.top;
btn.style.cursor = 'grabbing';
document.addEventListener('mousemove', onMove, {
passive: false
});
document.addEventListener('mouseup', onEnd);
document.addEventListener('touchmove', onMove, {
passive: false
});
document.addEventListener('touchend', onEnd);
}
function onMove(e) {
e.preventDefault();
const cx = e.touches ? e.touches[0].clientX : e.clientX;
const cy = e.touches ? e.touches[0].clientY : e.clientY;
const dx = cx - startX,
dy = cy - startY;
if (Math.abs(dx) > 3 || Math.abs(dy) > 3) isDragging = true;
if (!isDragging) return;
const newLeft = Math.max(0, Math.min(window.innerWidth - fab.offsetWidth, startLeft + dx));
const newTop = Math.max(0, Math.min(window.innerHeight - fab.offsetHeight, startTop + dy));
fab.style.left = newLeft + 'px';
fab.style.top = newTop + 'px';
}
function onEnd() {
btn.style.cursor = 'grab';
document.removeEventListener('mousemove', onMove);
document.removeEventListener('mouseup', onEnd);
document.removeEventListener('touchmove', onMove);
document.removeEventListener('touchend', onEnd);
if (isDragging) {
localStorage.setItem(LS_KEY, JSON.stringify({
left: parseInt(fab.style.left),
top: parseInt(fab.style.top),
}));
}
}
btn.addEventListener('mousedown', onStart);
btn.addEventListener('touchstart', onStart, {
passive: true
});
// 非拖拽时才打开面板
btn.addEventListener('click', () => {
if (isDragging) return;
const p = Alpine.$data(document.getElementById('baccarat-panel'));
if (!p) return;
openPanel() {
const panel = document.getElementById('baccarat-panel');
if (!panel) return;
const p = Alpine.$data(panel);
p.show = true;
if (p.phase === 'betting' && p.countdown > 0 && !p.countdownTimer) {
p.startCountdown();
}
});
}
// 首次尝试(元素可能已在 DOM 中)
document.addEventListener('DOMContentLoaded', attachDrag);
// Alpine 使 x-cloak 元素出现后再次尝试
document.addEventListener('alpine:initialized', () => setTimeout(attachDrag, 100));
})();
},
};
}
</script>
<style>
@@ -634,7 +582,7 @@
*/
updateFab(visible) {
const fab = document.getElementById('baccarat-fab');
if (fab) fab._x_dataStack[0].visible = visible;
if (fab) Alpine.$data(fab).visible = visible;
},
/**