- 完成对 scripts.blade.php 中非核心业务逻辑(钓鱼游戏、AI机器人、系统全局公告)的深度抽象隔离 - 修复抢红包逻辑中 setInterval 缺失时间参数(1000)引发浏览器前端主线程挂起的重度阻塞问题 - 修复 lottery-panel 组件结尾漏写 </div> 导致的连锁级渲染树崩溃(该崩溃导致红包节点被意外当作隐藏后代节点渲染,造成彻底不可见) - 对相关模板规范代码结构,执行 Laravel Pint 格式化并提交
423 lines
18 KiB
PHP
423 lines
18 KiB
PHP
{{--
|
||
文件功能:神秘箱子游戏前台UI组件
|
||
|
||
功能描述:
|
||
- 右下角悬浮提示标(检测到可领取箱子时显示,支持拖动移位)
|
||
- 监听聊天消息事件,识别公屏暗号提示后自动出现
|
||
- 用户在聊天框输入暗号(由前端拦截 /mystery-box/claim 接口)
|
||
- 或点击悬浮图标展开快速输入界面
|
||
- 开箱结果展示 toast 通知
|
||
--}}
|
||
|
||
{{-- ─── 神秘箱子悬浮提示(可拖动) ─── --}}
|
||
<div id="mystery-box-fab" x-data="mysteryBoxFab()" x-show="visible" x-cloak
|
||
:style="'position:fixed; left:' + posX + 'px; top:' + posY + 'px; z-index:9880; 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:56px; height:56px; border-radius:50%; border:none; position:relative;
|
||
font-size:24px; display:flex; align-items:center; justify-content:center;
|
||
animation:mb-pulse 1.8s ease-in-out infinite; user-select:none;"
|
||
:style="[
|
||
dragging ? 'cursor:grabbing;' : 'cursor:grab;',
|
||
boxType === 'rare' ?
|
||
'background:linear-gradient(135deg,#7c3aed,#a78bfa); box-shadow:0 4px 24px rgba(124,58,237,.6);' :
|
||
boxType === 'trap' ?
|
||
'background:linear-gradient(135deg,#7f1d1d,#dc2626); box-shadow:0 4px 24px rgba(220,38,38,.6);' :
|
||
'background:linear-gradient(135deg,#065f46,#10b981); box-shadow:0 4px 24px rgba(16,185,129,.6);'
|
||
]"
|
||
title="神秘箱子开箱中!(可拖动)">
|
||
<span x-text="boxEmoji">📦</span>
|
||
{{-- 倒计时badge --}}
|
||
<span x-show="secondsLeft !== null && secondsLeft <= 30"
|
||
style="position:absolute; top:-6px; right:-6px; background:#ef4444; color:#fff;
|
||
font-size:10px; font-weight:bold; border-radius:10px; padding:1px 5px; min-width:18px; text-align:center;"
|
||
x-text="secondsLeft">
|
||
</span>
|
||
</button>
|
||
|
||
{{-- 悬浮提示标签 --}}
|
||
<div style="position:absolute; left:62px; top:50%; transform:translateY(-50%);
|
||
background:rgba(0,0,0,.85); color:#fff; border-radius:8px; padding:4px 10px;
|
||
font-size:12px; white-space:nowrap; pointer-events:none;"
|
||
:style="boxType === 'rare' ? 'border:1px solid rgba(167,139,250,.4);' :
|
||
boxType === 'trap' ? 'border:1px solid rgba(239,68,68,.3);' :
|
||
'border:1px solid rgba(16,185,129,.3);'">
|
||
<span x-text="boxTypeName">神秘箱</span>
|
||
<span x-show="secondsLeft !== null" style="margin-left:4px; opacity:.7;" x-text="'⏰ ' + secondsLeft + 's'">
|
||
</span>
|
||
</div>
|
||
</div>
|
||
|
||
{{-- ─── 神秘箱子快捷输入面板 ─── --}}
|
||
<div id="mystery-box-panel" x-data="mysteryBoxPanel()" x-show="show" x-cloak>
|
||
<div x-transition:enter="transition ease-out duration-300" x-transition:enter-start="opacity-0 scale-95"
|
||
x-transition:enter-end="opacity-100 scale-100" x-transition:leave="transition ease-in duration-200"
|
||
x-transition:leave-start="opacity-100 scale-100" x-transition:leave-end="opacity-0 scale-95"
|
||
style="position:fixed; inset:0; background:rgba(0,0,0,.75); z-index:9920;
|
||
display:flex; align-items:center; justify-content:center;">
|
||
|
||
<div style="width:380px; max-width:94vw; border-radius:24px; overflow:hidden;
|
||
font-family:system-ui,sans-serif; position:relative;"
|
||
:style="boxType === 'rare' ?
|
||
'box-shadow:0 24px 80px rgba(124,58,237,.5); background:linear-gradient(180deg,#1e1b4b,#2e1065);' :
|
||
boxType === 'trap' ?
|
||
'box-shadow:0 24px 80px rgba(220,38,38,.4); background:linear-gradient(180deg,#1c0606,#2d0909);' :
|
||
'box-shadow:0 24px 80px rgba(16,185,129,.4); background:linear-gradient(180deg,#022c22,#064e3b);'">
|
||
|
||
{{-- ─── 顶部 ─── --}}
|
||
<div style="padding:20px 22px 16px;"
|
||
:style="boxType === 'rare' ?
|
||
'background:linear-gradient(135deg,#4c1d95,#6d28d9,#7c3aed);' :
|
||
boxType === 'trap' ?
|
||
'background:linear-gradient(135deg,#7f1d1d,#b91c1c,#dc2626);' :
|
||
'background:linear-gradient(135deg,#064e3b,#059669,#10b981);'">
|
||
|
||
<div style="display:flex; align-items:center; gap:12px;">
|
||
<div style="font-size:36px;" x-text="boxEmoji">📦</div>
|
||
<div>
|
||
<div style="color:#fff; font-weight:900; font-size:17px;" x-text="boxTypeName + ' 开箱!'"></div>
|
||
<div style="color:rgba(255,255,255,.6); font-size:12px; margin-top:2px;">
|
||
输入暗号即可开箱,限时
|
||
<span x-text="secondsLeft !== null ? secondsLeft + ' 秒' : ''"
|
||
style="color:#fbbf24; font-weight:bold;"></span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{{-- 倒计时进度条 --}}
|
||
<div x-show="secondsLeft !== null && totalSeconds > 0"
|
||
style="margin-top:10px; height:3px; background:rgba(255,255,255,.15); border-radius:2px; overflow:hidden;">
|
||
<div style="height:100%; border-radius:2px; transition:width 1s linear;"
|
||
:style="'width:' + Math.max(0, secondsLeft / totalSeconds * 100) + '%; background:' +
|
||
(boxType === 'rare' ? '#c4b5fd' : boxType === 'trap' ? '#fca5a5' : '#6ee7b7')">
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{{-- ─── 输入区 ─── --}}
|
||
<div style="padding:20px;">
|
||
{{-- 奖励提示 --}}
|
||
<div style="text-align:center; margin-bottom:14px; padding:10px; border-radius:12px;"
|
||
:style="boxType === 'rare' ?
|
||
'background:rgba(124,58,237,.15); border:1px solid rgba(167,139,250,.25);' :
|
||
boxType === 'trap' ?
|
||
'background:rgba(220,38,38,.1); border:1px solid rgba(248,113,113,.2);' :
|
||
'background:rgba(16,185,129,.1); border:1px solid rgba(52,211,153,.2);'">
|
||
<div style="font-size:13px; opacity:.8;"
|
||
x-text="
|
||
boxType === 'rare' ? '✨ 稀有箱 · 奖励丰厚,手速要快!' :
|
||
boxType === 'trap' ? '⚠️ 黑化箱 · 开了可能倒扣金币!谨慎开启!' :
|
||
'🎁 发送正确暗号即可开箱领奖'
|
||
"
|
||
style="color:rgba(255,255,255,.75);"></div>
|
||
</div>
|
||
|
||
{{-- 暗号输入框 --}}
|
||
<div style="margin-bottom:12px;">
|
||
<input type="text" x-model="inputCode" placeholder="请输入暗号(区分大小写)"
|
||
@keydown.enter="doClaimFromPanel()" @input="inputCode = $event.target.value.toUpperCase()"
|
||
style="width:100%; border-radius:10px; padding:12px 14px; font-size:15px;
|
||
font-weight:bold; text-align:center; letter-spacing:4px; text-transform:uppercase;
|
||
background:rgba(255,255,255,.1); border:2px solid rgba(255,255,255,.2);
|
||
color:#fff; box-sizing:border-box; outline:none; transition:border-color .15s;"
|
||
:style="boxType === 'rare' ? '&:focus { border-color:#a78bfa; }' : ''">
|
||
</div>
|
||
|
||
{{-- 提交按钮 --}}
|
||
<button @click="doClaimFromPanel()" :disabled="!inputCode.trim() || claiming"
|
||
style="width:100%; border:none; border-radius:12px; padding:13px; font-size:15px;
|
||
font-weight:900; cursor:pointer; transition:all .2s; letter-spacing:2px;"
|
||
:style="(!inputCode.trim() || claiming) ? {
|
||
background: 'rgba(255,255,255,.08)',
|
||
color: 'rgba(255,255,255,.3)',
|
||
cursor: 'not-allowed'
|
||
} : boxType === 'rare' ? {
|
||
background: 'linear-gradient(135deg,#7c3aed,#8b5cf6,#a78bfa)',
|
||
color: '#fff',
|
||
boxShadow: '0 4px 20px rgba(124,58,237,.5)'
|
||
} : boxType === 'trap' ? {
|
||
background: 'linear-gradient(135deg,#b91c1c,#dc2626,#ef4444)',
|
||
color: '#fff',
|
||
boxShadow: '0 4px 20px rgba(220,38,38,.5)'
|
||
} : {
|
||
background: 'linear-gradient(135deg,#059669,#10b981,#34d399)',
|
||
color: '#fff',
|
||
boxShadow: '0 4px 20px rgba(16,185,129,.5)'
|
||
}">
|
||
<span x-text="claiming ? '🎁 开箱中…' : '🎁 开箱!'"></span>
|
||
</button>
|
||
</div>
|
||
|
||
{{-- ─── 底部关闭 ─── --}}
|
||
<div style="padding:8px 20px 12px; display:flex; justify-content:center;">
|
||
<button @click="show = false"
|
||
style="padding:6px 24px; background:rgba(255,255,255,.06); border:none; border-radius:20px;
|
||
font-size:12px; color:rgba(255,255,255,.4); cursor:pointer; transition:all .15s;"
|
||
onmouseover="this.style.background='rgba(255,255,255,.12)'"
|
||
onmouseout="this.style.background='rgba(255,255,255,.06)'">
|
||
关闭
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<style>
|
||
@keyframes mb-pulse {
|
||
|
||
0%,
|
||
100% {
|
||
transform: scale(1);
|
||
}
|
||
|
||
50% {
|
||
transform: scale(1.08);
|
||
}
|
||
}
|
||
|
||
@keyframes mb-shake {
|
||
|
||
0%,
|
||
100% {
|
||
transform: translateX(0);
|
||
}
|
||
|
||
20%,
|
||
60% {
|
||
transform: translateX(-4px);
|
||
}
|
||
|
||
40%,
|
||
80% {
|
||
transform: translateX(4px);
|
||
}
|
||
}
|
||
</style>
|
||
|
||
<script>
|
||
/**
|
||
* 神秘箱子悬浮按钮 Alpine 组件
|
||
* 支持拖动 + 位置持久化 + 倒计时显示
|
||
*/
|
||
function mysteryBoxFab() {
|
||
const STORAGE_KEY = 'mystery_box_fab_pos';
|
||
const saved = JSON.parse(localStorage.getItem(STORAGE_KEY) || 'null');
|
||
|
||
return {
|
||
visible: false,
|
||
posX: saved?.x ?? 18,
|
||
posY: saved?.y ?? 240,
|
||
dragging: false,
|
||
_startX: 0,
|
||
_startY: 0,
|
||
_origX: 0,
|
||
_origY: 0,
|
||
_moved: false,
|
||
|
||
boxType: 'normal',
|
||
boxTypeName: '普通神秘箱',
|
||
boxEmoji: '📦',
|
||
secondsLeft: null,
|
||
totalSeconds: null,
|
||
_timer: null,
|
||
|
||
/**
|
||
* 初始化:轮询接口检测箱子
|
||
*/
|
||
init() {
|
||
this.checkStatus();
|
||
// 每5秒轮询一次
|
||
setInterval(() => this.checkStatus(), 5000);
|
||
},
|
||
|
||
/**
|
||
* 检查当前是否有可领取的箱子
|
||
*/
|
||
async checkStatus() {
|
||
try {
|
||
const res = await fetch('/mystery-box/status');
|
||
const data = await res.json();
|
||
|
||
if (data.active) {
|
||
this.visible = true;
|
||
this.boxType = data.box_type;
|
||
this.boxTypeName = data.type_name;
|
||
this.boxEmoji = data.type_emoji;
|
||
this.secondsLeft = data.seconds_left;
|
||
if (this.totalSeconds === null || data.seconds_left > this.totalSeconds) {
|
||
this.totalSeconds = data.seconds_left;
|
||
}
|
||
this.startCountdown();
|
||
|
||
// ── 全局标志:供聊天框暗号拦截使用 ──
|
||
window._mysteryBoxActive = true;
|
||
window._mysteryBoxPasscode = data.passcode ?? null;
|
||
|
||
// 同步面板数据
|
||
const panel = document.getElementById('mystery-box-panel');
|
||
if (panel) {
|
||
const pd = Alpine.$data(panel);
|
||
pd.boxType = data.box_type;
|
||
pd.boxTypeName = data.type_name;
|
||
pd.boxEmoji = data.type_emoji;
|
||
pd.secondsLeft = data.seconds_left;
|
||
pd.totalSeconds = this.totalSeconds;
|
||
}
|
||
} else {
|
||
this.visible = false;
|
||
clearInterval(this._timer);
|
||
|
||
// ── 清除全局标志 ──
|
||
window._mysteryBoxActive = false;
|
||
window._mysteryBoxPasscode = null;
|
||
|
||
// 关闭面板
|
||
const panel = document.getElementById('mystery-box-panel');
|
||
if (panel) Alpine.$data(panel).show = false;
|
||
}
|
||
} catch {}
|
||
},
|
||
|
||
/**
|
||
* 启动倒计时(本地)
|
||
*/
|
||
startCountdown() {
|
||
clearInterval(this._timer);
|
||
this._timer = setInterval(() => {
|
||
if (this.secondsLeft !== null && this.secondsLeft > 0) {
|
||
this.secondsLeft--;
|
||
// 同步面板
|
||
const panel = document.getElementById('mystery-box-panel');
|
||
if (panel) Alpine.$data(panel).secondsLeft = this.secondsLeft;
|
||
} else {
|
||
clearInterval(this._timer);
|
||
this.visible = false;
|
||
}
|
||
}, 1000);
|
||
},
|
||
|
||
/**
|
||
* 拖动开始
|
||
*/
|
||
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);
|
||
},
|
||
|
||
/**
|
||
* 拖动移动
|
||
*/
|
||
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 - 80, this._origX + dx));
|
||
this.posY = Math.max(4, Math.min(window.innerHeight - 70, this._origY + dy));
|
||
},
|
||
|
||
/**
|
||
* 拖动结束,非拖动时打开面板
|
||
*/
|
||
endDrag(e) {
|
||
if (!this.dragging) return;
|
||
this.dragging = false;
|
||
localStorage.setItem(STORAGE_KEY, JSON.stringify({
|
||
x: this.posX,
|
||
y: this.posY
|
||
}));
|
||
if (!this._moved) {
|
||
// 打开快捷输入面板
|
||
const panel = document.getElementById('mystery-box-panel');
|
||
if (panel) Alpine.$data(panel).show = true;
|
||
}
|
||
},
|
||
};
|
||
}
|
||
|
||
/**
|
||
* 神秘箱子快捷输入面板 Alpine 组件
|
||
*/
|
||
function mysteryBoxPanel() {
|
||
return {
|
||
show: false,
|
||
boxType: 'normal',
|
||
boxTypeName: '神秘箱',
|
||
boxEmoji: '📦',
|
||
secondsLeft: null,
|
||
totalSeconds: null,
|
||
inputCode: '',
|
||
claiming: false,
|
||
|
||
/**
|
||
* 从面板提交开箱暗号
|
||
*/
|
||
async doClaimFromPanel() {
|
||
if (!this.inputCode.trim() || this.claiming) return;
|
||
this.claiming = true;
|
||
|
||
const result = await window.mysteryBoxClaim(this.inputCode.trim());
|
||
|
||
this.claiming = false;
|
||
|
||
if (result.ok) {
|
||
this.show = false;
|
||
this.inputCode = '';
|
||
}
|
||
},
|
||
};
|
||
}
|
||
|
||
/**
|
||
* 全局开箱函数(可被聊天框暗号处理逻辑调用)
|
||
*
|
||
* @param {string} passcode 用户输入的暗号
|
||
* @returns {Promise<{ok: boolean, message?: string, reward?: number}>}
|
||
*/
|
||
window.mysteryBoxClaim = async function(passcode) {
|
||
try {
|
||
const res = await fetch('/mystery-box/claim', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'Accept': 'application/json',
|
||
'X-CSRF-TOKEN': document.querySelector('meta[name=csrf-token]')?.content,
|
||
},
|
||
body: JSON.stringify({
|
||
passcode
|
||
}),
|
||
});
|
||
const data = await res.json();
|
||
|
||
if (data.ok) {
|
||
// 成功动画提示
|
||
const isPositive = data.reward > 0;
|
||
window.chatDialog?.alert(
|
||
data.message || '开箱成功!',
|
||
isPositive ? '🎉 恭喜!' : '☠️ 中了陷阱!',
|
||
isPositive ? '#10b981' : '#ef4444',
|
||
);
|
||
// 更新全局金币余额
|
||
if (window.__chatUser) window.__chatUser.jjb = data.balance;
|
||
} else {
|
||
window.chatDialog?.alert(data.message || '开箱失败', '提示', '#f59e0b');
|
||
}
|
||
|
||
return data;
|
||
} catch {
|
||
window.chatDialog?.alert('网络异常,请稍后重试。', '错误', '#ef4444');
|
||
return {
|
||
ok: false
|
||
};
|
||
}
|
||
};
|
||
</script>
|