Files
chatroom/resources/views/chat/partials/slot-machine.blade.php
T
lkddi f45483bcba 功能更新与UI优化:游戏图标移除、用户名片修复、婚礼红包界面重设计
- 移除聊天室右下角浮动游戏图标(占卜、百家乐、赛马、老虎机)
- 用户名片按钮区:修复已婚/已好友时按钮换行问题,统一单行显示
- 婚礼红包弹窗:重设计为喜庆鲜红背景,领取按钮改为圆形米黄样式
- 新增婚礼红包恢复接口(/wedding/pending-envelopes),刷新后自动恢复领取按钮
- 修复 Alpine :style 字符串覆盖静态 style 导致圆形按钮失效的问题
- 撤职后用户等级改为根据经验值重新计算,不再无条件重置为1
- 管理员修改用户经验值后自动重算等级,有职务用户等级锁定
- 娱乐大厅钓鱼游戏按钮直接调用 startFishing() 简化操作流程
- 新增赛马、占卜、百家乐游戏及相关后端逻辑
2026-03-03 23:19:59 +08:00

543 lines
26 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
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全服广播)
- 每日次数限制、金币余额显示
- 最近记录展示
--}}
{{-- \u2500\u2500\u2500 \u8001\u864e\u673a\u4e3b\u9762\u677f \u2500\u2500\u2500 --}}
<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,.55); z-index:9950;
display:flex; align-items:center; justify-content:center;">
<div
style="width:420px; max-width:96vw; border-radius:8px; overflow:hidden;
box-shadow:0 8px 32px rgba(0,0,0,.3); font-family:'Microsoft YaHei',SimSun,sans-serif; background:#fff;">
{{-- \u2500\u2500\u2500 \u6807\u9898\u680f\uff08\u6d77\u519b\u84dd\u98ce\u683c\uff09\u2500\u2500\u2500 --}}
<div
style="background:linear-gradient(135deg,#336699,#5a8fc0); padding:10px 16px; display:flex; align-items:center; gap:10px;">
<div style="font-size:14px; font-weight:bold; color:#fff; flex:1;">🎰 老虎机</div>
<div
style="font-size:12px; color:rgba(255,255,255,.8); background:rgba(0,0,0,.15); padding:2px 10px; border-radius:10px; display:flex; align-items:center; gap:4px;">
🪙 <span x-text="Number(balance).toLocaleString()" style="color:#fef08a; font-weight:bold;"></span>
</div>
<div x-show="dailyLimit > 0"
style="font-size:11px; color:rgba(255,255,255,.7); background:rgba(255,255,255,.15); padding:2px 8px; border-radius:10px;"
x-text="'剩余 ' + remaining + ' 次'"></div>
<span onclick="Alpine.$data(document.getElementById('slot-panel')).close()"
style="cursor:pointer; font-size:18px; color:rgba(255,255,255,.8); line-height:1; transition:opacity .15s;"
onmouseover="this.style.opacity=1" onmouseout="this.style.opacity=.8">&times;</span>
</div>
{{-- \u2500\u2500\u2500 \u8de8\u8d39\u8bf4\u660e\u6761 \u2500\u2500\u2500 --}}
<div
style="background:#f6faff; border-bottom:1px solid #d0e4f5; padding:4px 16px; font-size:11px; color:#5a8fc0; text-align:center;">
每次消耗 <span x-text="costPerSpin" style="color:#336699; font-weight:bold;"></span> 金币
</div>
{{-- \u2500\u2500\u2500 \u4e3b\u4f53\u5185\u5bb9\uff08\u767d\u5e95\uff09\u2500\u2500\u2500 --}}
<div style="background:#fff; padding:16px 16px 12px;">
{{-- \u4e09\u5217\u8f6c\u8f6e --}}
<div
style="background:#f0f6ff; border-radius:10px; padding:14px 12px; margin-bottom:14px;
border:1px solid #d0e4f5; box-shadow:inset 0 2px 6px rgba(51,102,153,.06);">
<div style="display:flex; gap:8px; justify-content:center; align-items:center;">
{{-- 第一列 --}}
<div
style="flex:1; background:#fff; border-radius:8px;
height:120px; display:flex; align-items:center; justify-content:center;
border:1.5px solid #b8d0e8; box-shadow:0 2px 8px rgba(51,102,153,.1);">
<div id="slot-reel-0" x-text="spinning ? spinEmojis[0] : resultEmojis[0]"
:style="'font-size:80px; line-height:1; transition:all .15s; user-select:none; ' + (spinning &&
!reel1Stopped ? 'animation:reel-spin .1s linear infinite' : '')">
</div>
</div>
{{-- 分隔 --}}
<div style="color:#b8d0e8; font-size:20px; font-weight:900;">|</div>
{{-- 第二列 --}}
<div
style="flex:1; background:#fff; border-radius:8px;
height:120px; display:flex; align-items:center; justify-content:center;
border:1.5px solid #b8d0e8; box-shadow:0 2px 8px rgba(51,102,153,.1);">
<div id="slot-reel-1" x-text="spinning ? spinEmojis[1] : resultEmojis[1]"
:style="'font-size:80px; line-height:1; transition:all .15s; user-select:none; ' + (spinning &&
!reel2Stopped ? 'animation:reel-spin .12s linear infinite' : '')">
</div>
</div>
{{-- 分隔 --}}
<div style="color:#b8d0e8; font-size:20px; font-weight:900;">|</div>
{{-- 第三列 --}}
<div
style="flex:1; background:#fff; border-radius:8px;
height:120px; display:flex; align-items:center; justify-content:center;
border:1.5px solid #b8d0e8; box-shadow:0 2px 8px rgba(51,102,153,.1);">
<div id="slot-reel-2" x-text="spinning ? spinEmojis[2] : resultEmojis[2]"
:style="'font-size:80px; line-height:1; transition:all .15s; user-select:none; ' + (spinning &&
!reel3Stopped ? 'animation:reel-spin .14s linear infinite' : '')">
</div>
</div>
</div>
{{-- 中间射线指示条 --}}
<div
style="height:2px; background:linear-gradient(90deg,transparent,rgba(51,102,153,.3),transparent);
margin-top:10px; border-radius:1px;">
</div>
</div>
{{-- \u7ed3\u679c\u63d0\u793a --}}
<div style="text-align:center; min-height:34px; margin-bottom:10px;">
<div x-show="!spinning && resultLabel" x-transition
style="display:inline-block; padding:4px 18px; border-radius:20px; font-weight:bold; font-size:13px;"
:style="resultType === 'jackpot' ?
'background:linear-gradient(135deg,#fbbf24,#f59e0b); color:#fff; box-shadow:0 0 16px rgba(251,191,36,.4);' :
resultType === 'triple_gem' ?
'background:linear-gradient(135deg,#7c3aed,#a78bfa); color:#fff; box-shadow:0 0 12px rgba(124,58,237,.3);' :
resultType === 'triple' ?
'background:linear-gradient(135deg,#059669,#34d399); color:#fff; box-shadow:0 0 10px rgba(52,211,153,.3);' :
resultType === 'pair' ?
'background:linear-gradient(135deg,#336699,#5a8fc0); color:#fff; box-shadow:0 0 10px rgba(51,102,153,.2);' :
resultType === 'curse' ?
'background:linear-gradient(135deg,#dc2626,#ef4444); color:#fff; box-shadow:0 0 10px rgba(220,38,38,.3);' :
'background:#f0f6ff; color:#5a8fc0; border:1px solid #d0e4f5;'"
x-text="resultLabel">
</div>
<div x-show="spinning" style="color:#5a8fc0; font-size:13px; animation:blink .6s infinite;">
正在转动中…
</div>
</div>
{{-- \u76c8\u4e8f\u663e\u793a --}}
<div x-show="!spinning && resultType" style="text-align:center; margin-bottom:12px;">
<div x-show="netChange > 0" style="color:#16a34a; font-size:22px; font-weight:bold;"
x-text="'+' + Number(netChange).toLocaleString() + ' 🪙'">
</div>
<div x-show="netChange < 0" style="color:#dc2626; font-size:16px; font-weight:bold;"
x-text="Number(netChange).toLocaleString() + ' 🪙'">
</div>
<div x-show="netChange === 0 && resultType === 'miss'" style="color:#999; font-size:13px;">
损失 <span x-text="costPerSpin"></span> 金币
</div>
</div>
{{-- \u65cb\u8f6c\u6309\u94ae --}}
<button x-on:click="doSpin()" :disabled="spinning || (dailyLimit > 0 && remaining <= 0)"
style="display:block; width:100%; border:none; border-radius:12px; padding:12px 0;
font-size:15px; font-weight:bold; cursor:pointer; transition:all .2s; letter-spacing:1px;"
:style="(spinning || (dailyLimit > 0 && remaining <= 0)) ? {
background: '#e0e8f0',
color: '#99a8b8',
cursor: 'not-allowed',
boxShadow: 'none'
} : {
background: 'linear-gradient(135deg,#336699,#5a8fc0)',
color: '#fff',
boxShadow: '0 4px 14px rgba(51,102,153,.35)'
}">
<span
x-text="spinning ? '🎰 转动中…' :
(dailyLimit > 0 && remaining <= 0) ? '今日次数已用完 🔒' :
'🎰 SPIN'"></span>
</button>
{{-- \u8d54\u7387\u8bf4\u660e --}}
<div style="display:grid; grid-template-columns:repeat(4,1fr); gap:6px; margin-top:12px;">
<div
style="background:#fffbeb; border:1px solid #fde68a; border-radius:6px; padding:5px; text-align:center; font-size:10px;">
<div>7️⃣7️⃣7️⃣</div>
<div style="color:#d97706; font-weight:bold; margin-top:2px;">×100</div>
</div>
<div
style="background:#f5f3ff; border:1px solid #ddd6fe; border-radius:6px; padding:5px; text-align:center; font-size:10px;">
<div>💎💎💎</div>
<div style="color:#7c3aed; font-weight:bold; margin-top:2px;">×50</div>
</div>
<div
style="background:#f0fdf4; border:1px solid #bbf7d0; border-radius:6px; padding:5px; text-align:center; font-size:10px;">
<div>三同</div>
<div style="color:#059669; font-weight:bold; margin-top:2px;">×10</div>
</div>
<div
style="background:#f0f6ff; border:1px solid #d0e4f5; border-radius:6px; padding:5px; text-align:center; font-size:10px;">
<div>两同</div>
<div style="color:#336699; font-weight:bold; margin-top:2px;">×2</div>
</div>
</div>
{{-- 玩法说明(可折叠) --}}
<div x-data="{ open: false }" style="margin-top:12px;">
<button @click="open = !open"
style="width:100%; background:#f6faff; border:1px solid #d0e4f5; border-radius:8px;
padding:7px 12px; font-size:11px; color:#5a8fc0; cursor:pointer; transition:all .15s;
display:flex; align-items:center; justify-content:space-between;"
onmouseover="this.style.background='#eaf3ff'" onmouseout="this.style.background='#f6faff'">
<span>📖 玩法说明</span>
<span x-text="open ? '▲ 收起' : '▼ 展开'" style="font-size:10px; color:#99b0cc;"></span>
</button>
<div x-show="open" x-transition
style="margin-top:6px; background:#f6faff; border:1px solid #d0e4f5; border-radius:8px;
padding:10px 12px; font-size:11px; color:#446688; line-height:1.8;">
<div style="font-weight:bold; color:#336699; margin-bottom:6px; font-size:12px;">🎰 如何游玩</div>
<div> 点击 <strong>SPIN</strong> 按钮消耗金币抽奖,系统随机确定三列图案</div>
<div> 图案组合决定奖励倍率,奖励金币 = 消耗金币 × 倍率</div>
<div> 每日有次数上限,用完须等次日 0 点重置</div>
<div style="height:1px; background:#d0e4f5; margin:8px 0;"></div>
<div style="font-weight:bold; color:#336699; margin-bottom:6px; font-size:12px;">💎 图案赔率表</div>
<div style="display:grid; grid-template-columns:1fr 1fr; gap:4px;">
<div>🎰 7️⃣7️⃣7️⃣ <strong style="color:#d97706;">×100</strong> 大奖全服广播</div>
<div>💎💎💎 <strong style="color:#7c3aed;">×50</strong> 三钻大奖</div>
<div>任意三同 <strong style="color:#059669;">×10</strong> 三倍以上</div>
<div>任意两同 <strong style="color:#336699;">×2</strong> 回本加成</div>
<div>💀 骷髅 <strong style="color:#dc2626;">另有惩罚</strong></div>
<div>其余组合 <strong style="color:#999;">未中奖</strong></div>
</div>
<div style="height:1px; background:#d0e4f5; margin:8px 0;"></div>
<div style="font-weight:bold; color:#336699; margin-bottom:4px; font-size:12px;">⚠️ 注意事项</div>
<div> 奖励直接到账金币,无需额外领取</div>
<div> 三个 7️⃣ 为聊天室大奖,系统会全服广播</div>
<div> 未中奖只损失本次消耗的旋转费用</div>
</div>
</div>
{{-- 历史记录 --}}
<div x-show="history.length > 0" style="margin-top:12px;">
<div style="color:#99b0cc; font-size:10px; margin-bottom:5px;">最近记录</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:#f0f6ff; border:1px solid #d0e4f5; 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:#16a34a; font-weight:bold;' : 'color:#dc2626;'"
x-text="(h.payout > 0 ? '+' : '') + h.payout"></span>
</div>
</template>
</div>
</div>
</div>
{{-- \u2500\u2500\u2500 \u5e95\u90e8\u5173\u95ed \u2500\u2500\u2500 --}}
<div
style="background:#fff; border-top:1px solid #d0e4f5; padding:14px 16px; display:flex; justify-content:center;">
<button x-on:click="close()"
style="padding:10px 48px; min-width:140px; background:#f0f6ff; border:1px solid #b0d0ee; border-radius:12px;
font-size:14px; font-weight:bold; color:#336699; cursor:pointer; transition:all .15s;"
onmouseover="this.style.background='#ddeeff'" onmouseout="this.style.background='#f0f6ff'">
关闭
</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() {
const STORAGE_KEY = 'slot_fab_pos';
const saved = JSON.parse(localStorage.getItem(STORAGE_KEY) || 'null');
return {
visible: false,
posX: saved?.x ?? 18,
posY: saved?.y ?? 150,
dragging: false,
_startX: 0,
_startY: 0,
_origX: 0,
_origY: 0,
_moved: false,
async init() {
try {
const res = await fetch('/slot/info');
const data = await res.json();
this.visible = data.enabled === true;
} catch {}
},
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 - 60, this._origX - dx));
this.posY = Math.max(4, Math.min(window.innerHeight - 60, 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) this.openPanel();
},
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: [],
/**
* Alpine 初始化: 监听 show 变化自动加载数据(解决从游戏大厅入口不调用 open() 时历史不刷新的问题)
*/
init() {
this.$watch('show', async (val) => {
if (val) {
await this.loadInfo();
await this.loadHistory();
}
});
},
/**
* 打开面板并加载数据
*/
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>