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:
@@ -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;
|
||||
},
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user