Files
chatroom/resources/views/chat/partials/slot-machine.blade.php

461 lines
21 KiB
PHP
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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.
{{--
文件功能:老虎机游戏前台面板组件
聊天室内老虎机游戏:
- 悬浮按钮 🎰 入口(游戏开启时显示)
- 三列滚轮动画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>