461 lines
21 KiB
PHP
461 lines
21 KiB
PHP
{{--
|
||
文件功能:老虎机游戏前台面板组件
|
||
|
||
聊天室内老虎机游戏:
|
||
- 悬浮按钮 🎰 入口(游戏开启时显示)
|
||
- 三列滚轮动画(CSS 逐列延迟停止)
|
||
- 权重随机图案、多种赔率(三7全服广播)
|
||
- 每日次数限制、金币余额显示
|
||
- 最近记录展示
|
||
--}}
|
||
|
||
{{-- ─── 老虎机悬浮按钮 ─── --}}
|
||
<div id="slot-fab" x-data="slotFab()" x-show="visible" x-cloak
|
||
style="position:fixed; bottom:150px; right:18px; z-index:9900;">
|
||
<button x-on:click="openPanel()"
|
||
style="width:52px; height:52px; border-radius:50%; border:none; cursor:pointer;
|
||
background:linear-gradient(135deg,#d97706,#f59e0b);
|
||
box-shadow:0 4px 20px rgba(245,158,11,.5);
|
||
font-size:22px; display:flex; align-items:center; justify-content:center;
|
||
animation:slot-pulse 2s infinite;"
|
||
title="老虎机">🎰</button>
|
||
</div>
|
||
|
||
{{-- ─── 老虎机主面板 ─── --}}
|
||
<div id="slot-panel" x-data="slotPanel()" 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:9950;
|
||
display:flex; align-items:center; justify-content:center;">
|
||
|
||
<div
|
||
style="width:400px; max-width:96vw; border-radius:24px; overflow:hidden;
|
||
box-shadow:0 24px 80px rgba(245,158,11,.4); font-family:system-ui,sans-serif;">
|
||
|
||
{{-- ─── 顶部标题 ─── --}}
|
||
<div style="background:linear-gradient(135deg,#78350f,#b45309,#d97706); padding:16px 20px 12px;">
|
||
<div style="display:flex; align-items:center; justify-content:space-between;">
|
||
<div>
|
||
<div style="color:#fff; font-weight:900; font-size:18px;">🎰 老虎机</div>
|
||
<div style="color:rgba(255,255,255,.6); font-size:11px; margin-top:2px;">
|
||
每次消耗 <span x-text="costPerSpin" style="color:#fbbf24; font-weight:bold;"></span> 金币
|
||
</div>
|
||
</div>
|
||
<div style="text-align:right;">
|
||
<div style="color:#fbbf24; font-size:18px; font-weight:900;">🪙 <span
|
||
x-text="Number(balance).toLocaleString()"></span></div>
|
||
<div x-show="dailyLimit > 0" style="color:rgba(255,255,255,.5); font-size:11px;"
|
||
x-text="'今日剩余 ' + remaining + ' 次'"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{{-- ─── 滚轮区域 ─── --}}
|
||
<div style="background:linear-gradient(180deg,#1c1007,#292012); padding:20px;">
|
||
|
||
{{-- 三列转轮 --}}
|
||
<div
|
||
style="background:#0f0a02; border-radius:16px; padding:16px 12px; margin-bottom:16px;
|
||
border:2px solid rgba(245,158,11,.3); box-shadow:inset 0 0 30px rgba(0,0,0,.5);">
|
||
<div style="display:flex; gap:8px; justify-content:center; align-items:center;">
|
||
{{-- 第一列 --}}
|
||
<div
|
||
style="flex:1; background:linear-gradient(180deg,#1a1000,#2d1f00); border-radius:12px;
|
||
height:90px; display:flex; align-items:center; justify-content:center;
|
||
border:1px solid rgba(245,158,11,.2); overflow:hidden; position:relative;">
|
||
<div id="slot-reel-0" style="font-size:44px; transition:all .15s; user-select:none;"
|
||
x-text="spinning ? spinEmojis[0] : resultEmojis[0]"
|
||
:style="spinning && !reel1Stopped ? 'animation:reel-spin .1s linear infinite' : ''">
|
||
</div>
|
||
</div>
|
||
{{-- 分隔 --}}
|
||
<div style="color:rgba(245,158,11,.4); font-size:20px; font-weight:900;">|</div>
|
||
{{-- 第二列 --}}
|
||
<div
|
||
style="flex:1; background:linear-gradient(180deg,#1a1000,#2d1f00); border-radius:12px;
|
||
height:90px; display:flex; align-items:center; justify-content:center;
|
||
border:1px solid rgba(245,158,11,.2); overflow:hidden; position:relative;">
|
||
<div id="slot-reel-1" style="font-size:44px; transition:all .15s; user-select:none;"
|
||
x-text="spinning ? spinEmojis[1] : resultEmojis[1]"
|
||
:style="spinning && !reel2Stopped ? 'animation:reel-spin .12s linear infinite' : ''">
|
||
</div>
|
||
</div>
|
||
{{-- 分隔 --}}
|
||
<div style="color:rgba(245,158,11,.4); font-size:20px; font-weight:900;">|</div>
|
||
{{-- 第三列 --}}
|
||
<div
|
||
style="flex:1; background:linear-gradient(180deg,#1a1000,#2d1f00); border-radius:12px;
|
||
height:90px; display:flex; align-items:center; justify-content:center;
|
||
border:1px solid rgba(245,158,11,.2); overflow:hidden; position:relative;">
|
||
<div id="slot-reel-2" style="font-size:44px; transition:all .15s; user-select:none;"
|
||
x-text="spinning ? spinEmojis[2] : resultEmojis[2]"
|
||
:style="spinning && !reel3Stopped ? 'animation:reel-spin .14s linear infinite' : ''">
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{{-- 中间射线指示条 --}}
|
||
<div
|
||
style="height:2px; background:linear-gradient(90deg,transparent,rgba(245,158,11,.6),transparent);
|
||
margin-top:8px; border-radius:1px;">
|
||
</div>
|
||
</div>
|
||
|
||
{{-- 结果提示 --}}
|
||
<div style="text-align:center; min-height:36px; margin-bottom:12px;">
|
||
<div x-show="!spinning && resultLabel" x-transition
|
||
style="display:inline-block; padding:5px 18px; border-radius:20px; font-weight:bold; font-size:14px;"
|
||
:style="resultType === 'jackpot' ?
|
||
'background:linear-gradient(135deg,#fbbf24,#f59e0b); color:#1c1007; box-shadow:0 0 20px rgba(251,191,36,.5);' :
|
||
resultType === 'triple_gem' ?
|
||
'background:rgba(167,139,250,.2); color:#c4b5fd; border:1px solid rgba(167,139,250,.3);' :
|
||
resultType === 'triple' ?
|
||
'background:rgba(52,211,153,.15); color:#6ee7b7; border:1px solid rgba(52,211,153,.25);' :
|
||
resultType === 'pair' ?
|
||
'background:rgba(96,165,250,.15); color:#93c5fd; border:1px solid rgba(96,165,250,.25);' :
|
||
resultType === 'curse' ?
|
||
'background:rgba(239,68,68,.15); color:#f87171; border:1px solid rgba(239,68,68,.25);' :
|
||
'background:rgba(255,255,255,.06); color:rgba(255,255,255,.4); '"
|
||
x-text="resultLabel">
|
||
</div>
|
||
<div x-show="spinning"
|
||
style="color:rgba(255,255,255,.4); font-size:13px; animation:blink .6s infinite;">
|
||
正在转动中…
|
||
</div>
|
||
</div>
|
||
|
||
{{-- 盈亏显示 --}}
|
||
<div x-show="!spinning && resultType" style="text-align:center; margin-bottom:12px;">
|
||
<div x-show="netChange > 0" style="color:#34d399; font-size:20px; font-weight:bold;"
|
||
x-text="'+' + Number(netChange).toLocaleString() + ' 🪙'">
|
||
</div>
|
||
<div x-show="netChange < 0" style="color:#f87171; font-size:16px; font-weight:bold;"
|
||
x-text="Number(netChange).toLocaleString() + ' 🪙'">
|
||
</div>
|
||
<div x-show="netChange === 0 && resultType === 'miss'"
|
||
style="color:rgba(255,255,255,.3); font-size:13px;">
|
||
损失 <span x-text="costPerSpin"></span> 金币
|
||
</div>
|
||
</div>
|
||
|
||
{{-- 转动按钮 --}}
|
||
<button x-on:click="doSpin()" :disabled="spinning || (dailyLimit > 0 && remaining <= 0)"
|
||
style="width:100%; border:none; border-radius:14px; padding:14px; font-size:16px;
|
||
font-weight:900; cursor:pointer; transition:all .2s; letter-spacing:2px;"
|
||
:style="(spinning || (dailyLimit > 0 && remaining <= 0)) ? {
|
||
background: '#292012',
|
||
color: 'rgba(255,255,255,.3)',
|
||
cursor: 'not-allowed'
|
||
} : {
|
||
background: 'linear-gradient(135deg,#b45309,#d97706,#f59e0b)',
|
||
color: '#fff',
|
||
boxShadow: '0 4px 20px rgba(245,158,11,.5)',
|
||
transform: 'scale(1)'
|
||
}">
|
||
<span
|
||
x-text="spinning ? '🎰 转动中…' :
|
||
(dailyLimit > 0 && remaining <= 0) ? '今日次数已用完 🔒' :
|
||
'🎰 SPIN'"></span>
|
||
</button>
|
||
|
||
{{-- 赔率说明 --}}
|
||
<div style="display:grid; grid-template-columns:repeat(4,1fr); gap:4px; margin-top:10px;">
|
||
<div
|
||
style="background:rgba(245,158,11,.1); border-radius:6px; padding:4px; text-align:center; font-size:10px;">
|
||
<div>7️⃣7️⃣7️⃣</div>
|
||
<div style="color:#fbbf24; font-weight:bold;">×100</div>
|
||
</div>
|
||
<div
|
||
style="background:rgba(167,139,250,.1); border-radius:6px; padding:4px; text-align:center; font-size:10px;">
|
||
<div>💎💎💎</div>
|
||
<div style="color:#c4b5fd; font-weight:bold;">×50</div>
|
||
</div>
|
||
<div
|
||
style="background:rgba(52,211,153,.1); border-radius:6px; padding:4px; text-align:center; font-size:10px;">
|
||
<div>三同</div>
|
||
<div style="color:#6ee7b7; font-weight:bold;">×10</div>
|
||
</div>
|
||
<div
|
||
style="background:rgba(96,165,250,.1); border-radius:6px; padding:4px; text-align:center; font-size:10px;">
|
||
<div>两同</div>
|
||
<div style="color:#93c5fd; font-weight:bold;">×2</div>
|
||
</div>
|
||
</div>
|
||
|
||
{{-- 历史记录 --}}
|
||
<div x-show="history.length > 0" style="margin-top:10px;">
|
||
<div style="color:rgba(255,255,255,.3); font-size:10px; margin-bottom:4px;">最近记录</div>
|
||
<div style="display:flex; gap:4px; flex-wrap:wrap;">
|
||
<template x-for="h in history" :key="h.created_at + h.result_label">
|
||
<div style="background:rgba(255,255,255,.06); border-radius:6px; padding:3px 8px;
|
||
font-size:11px; display:flex; align-items:center; gap:4px;"
|
||
:title="h.result_label + ' ' + (h.payout > 0 ? '+' : '') + h.payout">
|
||
<span x-text="h.emojis.join('')"></span>
|
||
<span :style="h.payout > 0 ? 'color:#4ade80' : 'color:#f87171'"
|
||
x-text="(h.payout > 0 ? '+' : '') + h.payout"></span>
|
||
</div>
|
||
</template>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{{-- ─── 底部关闭 ─── --}}
|
||
<div style="background:rgba(15,8,0,.95); padding:8px 20px; display:flex; justify-content:center;">
|
||
<button x-on:click="close()"
|
||
style="padding:6px 28px; 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 slot-pulse {
|
||
|
||
0%,
|
||
100% {
|
||
box-shadow: 0 4px 20px rgba(245, 158, 11, .5);
|
||
}
|
||
|
||
50% {
|
||
box-shadow: 0 4px 30px rgba(245, 158, 11, .9);
|
||
}
|
||
}
|
||
|
||
@keyframes reel-spin {
|
||
0% {
|
||
transform: translateY(-4px);
|
||
}
|
||
|
||
50% {
|
||
transform: translateY(4px);
|
||
}
|
||
|
||
100% {
|
||
transform: translateY(-4px);
|
||
}
|
||
}
|
||
|
||
@keyframes blink {
|
||
|
||
0%,
|
||
100% {
|
||
opacity: 1;
|
||
}
|
||
|
||
50% {
|
||
opacity: 0.4;
|
||
}
|
||
}
|
||
</style>
|
||
|
||
<script>
|
||
/**
|
||
* 老虎机悬浮按钮 Alpine 组件(检查游戏是否开启)
|
||
*/
|
||
function slotFab() {
|
||
return {
|
||
visible: false,
|
||
async init() {
|
||
try {
|
||
const res = await fetch('/slot/info');
|
||
const data = await res.json();
|
||
this.visible = data.enabled === true;
|
||
} catch {}
|
||
},
|
||
openPanel() {
|
||
const panel = document.getElementById('slot-panel');
|
||
if (panel) Alpine.$data(panel).open();
|
||
},
|
||
};
|
||
}
|
||
|
||
/**
|
||
* 老虎机主面板 Alpine 组件
|
||
*/
|
||
function slotPanel() {
|
||
// 所有图案的 emoji 数组,与服务端权重一致(用于转动时随机展示)
|
||
const ALL_EMOJIS = ['🍒', '🍋', '🍊', '🍇', '🔔', '💎', '💀', '7️⃣'];
|
||
|
||
return {
|
||
show: false,
|
||
|
||
// 配置
|
||
costPerSpin: 100,
|
||
dailyLimit: 0,
|
||
remaining: null,
|
||
balance: 0,
|
||
|
||
// 转轮状态
|
||
spinning: false,
|
||
reel1Stopped: false,
|
||
reel2Stopped: false,
|
||
reel3Stopped: false,
|
||
spinEmojis: ['🎰', '🎰', '🎰'],
|
||
resultEmojis: ['❓', '❓', '❓'],
|
||
|
||
// 结果
|
||
resultType: '',
|
||
resultLabel: '',
|
||
netChange: 0,
|
||
|
||
// 历史
|
||
history: [],
|
||
|
||
// 动画定时器
|
||
_spinInterval: null,
|
||
_stopTimers: [],
|
||
|
||
/**
|
||
* 打开面板并加载数据
|
||
*/
|
||
async open() {
|
||
this.show = true;
|
||
await this.loadInfo();
|
||
await this.loadHistory();
|
||
},
|
||
|
||
/**
|
||
* 加载游戏配置和余额
|
||
*/
|
||
async loadInfo() {
|
||
try {
|
||
const res = await fetch('/slot/info');
|
||
const data = await res.json();
|
||
if (!data.enabled) {
|
||
this.show = false;
|
||
return;
|
||
}
|
||
this.costPerSpin = data.cost_per_spin;
|
||
this.dailyLimit = data.daily_limit;
|
||
this.remaining = data.remaining;
|
||
// 从聊天室全局变量读取余额
|
||
this.balance = window.__chatUser?.jjb ?? 0;
|
||
} catch {}
|
||
},
|
||
|
||
/**
|
||
* 加载历史记录
|
||
*/
|
||
async loadHistory() {
|
||
try {
|
||
const res = await fetch('/slot/history');
|
||
const data = await res.json();
|
||
this.history = data.history || [];
|
||
} catch {}
|
||
},
|
||
|
||
/**
|
||
* 执行转动
|
||
*/
|
||
async doSpin() {
|
||
if (this.spinning) return;
|
||
if (this.dailyLimit > 0 && this.remaining <= 0) return;
|
||
|
||
this.spinning = true;
|
||
this.resultType = '';
|
||
this.resultLabel = '';
|
||
this.netChange = 0;
|
||
this.reel1Stopped = false;
|
||
this.reel2Stopped = false;
|
||
this.reel3Stopped = false;
|
||
|
||
// 开始随机滚动动画
|
||
this._spinInterval = setInterval(() => {
|
||
this.spinEmojis = [
|
||
ALL_EMOJIS[Math.floor(Math.random() * ALL_EMOJIS.length)],
|
||
ALL_EMOJIS[Math.floor(Math.random() * ALL_EMOJIS.length)],
|
||
ALL_EMOJIS[Math.floor(Math.random() * ALL_EMOJIS.length)],
|
||
];
|
||
}, 80);
|
||
|
||
// 请求后端
|
||
let data;
|
||
try {
|
||
const res = await fetch('/slot/spin', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'Accept': 'application/json',
|
||
'X-CSRF-TOKEN': document.querySelector('meta[name=csrf-token]')?.content,
|
||
},
|
||
});
|
||
data = await res.json();
|
||
} catch {
|
||
clearInterval(this._spinInterval);
|
||
this.spinning = false;
|
||
window.chatDialog?.alert('网络异常,请稍后重试', '错误', '#ef4444');
|
||
return;
|
||
}
|
||
|
||
if (!data.ok) {
|
||
clearInterval(this._spinInterval);
|
||
this.spinning = false;
|
||
window.chatDialog?.alert(data.message || '转动失败', '提示', '#ef4444');
|
||
return;
|
||
}
|
||
|
||
// 逐列停止(延迟效果)
|
||
const stopReel = (reelIndex, emoji, delay) => {
|
||
return new Promise(resolve => setTimeout(() => {
|
||
clearInterval(this._spinInterval);
|
||
this['reel' + (reelIndex + 1) + 'Stopped'] = true;
|
||
this.spinEmojis = [...this.spinEmojis];
|
||
this.spinEmojis[reelIndex] = emoji;
|
||
resolve();
|
||
}, delay));
|
||
};
|
||
|
||
await stopReel(0, data.emojis[0], 600);
|
||
this._spinInterval = setInterval(() => {
|
||
this.spinEmojis = [data.emojis[0],
|
||
ALL_EMOJIS[Math.floor(Math.random() * ALL_EMOJIS.length)],
|
||
ALL_EMOJIS[Math.floor(Math.random() * ALL_EMOJIS.length)]
|
||
];
|
||
}, 80);
|
||
await stopReel(1, data.emojis[1], 500);
|
||
this._spinInterval = setInterval(() => {
|
||
this.spinEmojis = [data.emojis[0], data.emojis[1],
|
||
ALL_EMOJIS[Math.floor(Math.random() * ALL_EMOJIS.length)]
|
||
];
|
||
}, 80);
|
||
await stopReel(2, data.emojis[2], 400);
|
||
|
||
clearInterval(this._spinInterval);
|
||
|
||
// 显示结果
|
||
this.resultEmojis = data.emojis;
|
||
this.resultType = data.result_type;
|
||
this.resultLabel = data.result_label;
|
||
this.netChange = data.payout > 0 ? data.payout - this.costPerSpin : -this.costPerSpin + (data
|
||
.payout < 0 ? data.payout : 0);
|
||
this.balance = data.balance;
|
||
this.spinning = false;
|
||
|
||
// 更新全局余额(让聊天界面同步)
|
||
if (window.__chatUser) window.__chatUser.jjb = data.balance;
|
||
|
||
if (this.dailyLimit > 0 && this.remaining !== null) {
|
||
this.remaining = Math.max(0, this.remaining - 1);
|
||
}
|
||
|
||
await this.loadHistory();
|
||
},
|
||
|
||
/**
|
||
* 关闭面板
|
||
*/
|
||
close() {
|
||
clearInterval(this._spinInterval);
|
||
this.spinning = false;
|
||
this.show = false;
|
||
},
|
||
};
|
||
}
|
||
</script>
|