Files
chatroom/resources/views/chat/partials/games/red-packet-panel.blade.php
T
lkddi bfb1a3bca4 重构(chat): 聊天室 Partials 第二阶段分类拆分及修复红包弹窗隐藏 Bug
- 完成对 scripts.blade.php 中非核心业务逻辑(钓鱼游戏、AI机器人、系统全局公告)的深度抽象隔离
- 修复抢红包逻辑中 setInterval 缺失时间参数(1000)引发浏览器前端主线程挂起的重度阻塞问题
- 修复 lottery-panel 组件结尾漏写 </div> 导致的连锁级渲染树崩溃(该崩溃导致红包节点被意外当作隐藏后代节点渲染,造成彻底不可见)
- 对相关模板规范代码结构,执行 Laravel Pint 格式化并提交
2026-03-09 11:30:11 +08:00

689 lines
25 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.
{{--
文件功能:礼包红包弹窗面板(HTML + CSS + 交互脚本)
包含:
1. 红包遮罩弹窗 DOM#red-packet-modal
2. 红包弹窗样式(CSS
3. 红包前端交互 JS(发包、抢包、倒计时、WebSocket 监听)
全局函数:
window.sendRedPacket() superlevel 发包(弹 chatBanner 选类型)
window.showRedPacketModal(...) 展示红包弹窗(收到 WebSocket 事件触发)
window.closeRedPacketModal() 关闭红包弹窗
window.claimRedPacket() 用户抢红包
window.updateRedPacketClaimsUI() 更新领取名单(WebSocket 广播后调用)
注:依赖 window.chatBannerchat-banner.blade.php)、window.chatDialog、window.chatToast。
scripts.blade.php 提取,与其他游戏面板(baccarat-panel、slot-machine 等)保持统一结构。
@author ChatRoom Laravel
@version 1.0.0
--}}
<style>
/* 红包弹窗遮罩 */
#red-packet-modal {
display: none;
position: fixed !important;
inset: 0 !important;
z-index: 10500 !important;
background: rgba(0, 0, 0, 0.6) !important;
justify-content: center !important;
align-items: center !important;
animation: rpFadeIn 0.25s ease;
}
@keyframes rpFadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
/* 红包卡片主体 */
#red-packet-card {
width: 300px;
border-radius: 16px;
overflow: hidden;
box-shadow: 0 16px 48px rgba(0, 0, 0, 0.5), 0 0 0 2px rgba(220, 38, 38, 0.4);
animation: rpCardIn 0.35s cubic-bezier(0.34, 1.56, 0.64, 1);
position: relative;
}
@keyframes rpCardIn {
from {
transform: scale(0.7) translateY(40px);
opacity: 0;
}
to {
transform: scale(1) translateY(0);
opacity: 1;
}
}
/* 红包顶部区 */
#rp-header {
background: linear-gradient(160deg, #dc2626 0%, #b91c1c 50%, #991b1b 100%);
padding: 24px 20px 20px;
text-align: center;
position: relative;
}
#rp-header::before {
content: '';
position: absolute;
inset: 0;
background: radial-gradient(ellipse at 50% 0%, rgba(255, 100, 0, 0.3) 0%, transparent 70%);
pointer-events: none;
}
.rp-emoji {
font-size: 52px;
display: block;
filter: drop-shadow(0 4px 8px rgba(0, 0, 0, 0.3));
animation: rpBounce 1.5s ease-in-out infinite;
}
@keyframes rpBounce {
0%,
100% {
transform: translateY(0);
}
50% {
transform: translateY(-6px);
}
}
.rp-sender {
color: #fde68a;
font-size: 13px;
margin-top: 8px;
font-weight: bold;
}
.rp-title {
color: #fff;
font-size: 18px;
font-weight: bold;
margin-top: 4px;
letter-spacing: 1px;
}
.rp-amount {
color: #fde68a;
font-size: 28px;
font-weight: bold;
margin-top: 6px;
text-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
}
.rp-amount small {
font-size: 14px;
opacity: 0.85;
}
/* 红包底部区 */
#rp-body {
background: #fff8f0;
padding: 16px 20px;
}
.rp-info-row {
display: flex;
justify-content: space-between;
font-size: 12px;
color: #92400e;
margin-bottom: 8px;
}
/* 倒计时条 */
#rp-timer-bar-wrap {
background: #fee2e2;
border-radius: 4px;
height: 6px;
overflow: hidden;
margin-bottom: 14px;
}
#rp-timer-bar {
height: 100%;
background: linear-gradient(90deg, #dc2626, #f97316);
border-radius: 4px;
transition: width 1s linear;
}
/* 领取按钮 */
#rp-claim-btn {
width: 100%;
padding: 13px;
background: linear-gradient(135deg, #dc2626, #ea580c);
color: #fff;
font-size: 16px;
font-weight: bold;
border: none;
border-radius: 10px;
cursor: pointer;
letter-spacing: 1px;
box-shadow: 0 4px 12px rgba(220, 38, 38, 0.4);
transition: opacity .2s, transform .15s;
}
#rp-claim-btn:hover {
opacity: 0.9;
transform: translateY(-1px);
}
#rp-claim-btn:active {
transform: translateY(0);
}
#rp-claim-btn:disabled {
background: #9ca3af;
box-shadow: none;
cursor: not-allowed;
transform: none;
}
/* 已领取名单 */
#rp-claims-list {
margin-top: 12px;
max-height: 100px;
overflow-y: auto;
border-top: 1px dashed #fca5a5;
padding-top: 8px;
}
.rp-claim-item {
display: flex;
justify-content: space-between;
font-size: 11px;
color: #555;
padding: 2px 0;
}
.rp-claim-item span:last-child {
color: #dc2626;
font-weight: bold;
}
/* 关闭按钮 */
#rp-close-btn {
position: absolute;
top: 10px;
right: 12px;
color: rgba(255, 255, 255, 0.7);
font-size: 20px;
cursor: pointer;
line-height: 1;
}
#rp-close-btn:hover {
color: #fff;
}
/* 状态提示 */
#rp-status-msg {
font-size: 12px;
text-align: center;
margin-top: 8px;
min-height: 16px;
color: #16a34a;
font-weight: bold;
}
</style>
{{-- 红包弹窗 DOM --}}
<div id="red-packet-modal">
<div id="red-packet-card">
{{-- 顶部标题区 --}}
<div id="rp-header">
<span id="rp-close-btn" onclick="closeRedPacketModal()"></span>
<span class="rp-emoji">🧧</span>
<div class="rp-sender" id="rp-sender-name">xxx 的礼包</div>
<div class="rp-title">聊天室专属礼包</div>
<div class="rp-amount"><small>总计 </small><span id="rp-total-amount">888</span><small id="rp-type-label">
金币</small></div>
</div>
{{-- 底部操作区 --}}
<div id="rp-body">
<div class="rp-info-row">
<span>剩余份数:<b id="rp-remaining">10</b> / <span id="rp-total-count">10</span> </span>
<span>倒计时:<b id="rp-countdown">120</b>s</span>
</div>
<div id="rp-timer-bar-wrap">
<div id="rp-timer-bar" style="width:100%;"></div>
</div>
<button id="rp-claim-btn" onclick="claimRedPacket()">🧧 立即抢红包</button>
<div id="rp-status-msg"></div>
{{-- 领取名单 --}}
<div id="rp-claims-list" style="display:none;">
<div style="font-size:11px; color:#92400e; margin-bottom:4px; font-weight:bold;">已领取名单:</div>
<div id="rp-claims-items"></div>
</div>
</div>
</div>
</div>
<script>
/**
* 礼包红包前端交互模块
*
* 功能:
* 1. sendRedPacket() — superlevel 点击「礼包」按钮后确认发包
* 2. showRedPacketModal() — 收到 RedPacketSent 事件后弹出红包卡片
* 3. claimRedPacket() — 用户点击「立即抢红包」
* 4. closeRedPacketModal() — 关闭红包弹窗
* 5. WebSocket 监听 — 监听 red-packet.sent 广播事件
*/
(function() {
'use strict';
// 当前红包状态
let _rpEnvelopeId = null; // 当前红包 ID
let _rpExpireAt = null; // 过期时间戳(ms
let _rpTotalSeconds = 120; // 总倒计时秒数
let _rpTimer = null; // 倒计时定时器
let _rpClaimed = false; // 本次会话是否已领取
let _rpType = 'gold'; // 当前红包类型(gold / exp
// ── 发包确认 ───────────────────────────────────────
/**
* superlevel 点击「礼包」按钮,弹出 chatBanner 三按钮选择类型后发包。
*/
window.sendRedPacket = function() {
window.chatBanner.show({
icon: '🧧',
title: '发出礼包',
name: '选择礼包类型',
body: '将发出 <b>888</b> 数量共 <b>10</b> 份的礼包,系统凭空发放,房间成员先到先得!',
gradient: ['#991b1b', '#dc2626', '#ea580c'],
titleColor: '#fde68a',
autoClose: 0,
buttons: [{
label: '💰 金币礼包',
color: '#d97706',
onClick(btn, close) {
close();
doSendRedPacket('gold');
},
},
{
label: '✨ 经验礼包',
color: '#7c3aed',
onClick(btn, close) {
close();
doSendRedPacket('exp');
},
},
{
label: '取消',
color: 'rgba(255,255,255,0.15)',
onClick(btn, close) {
close();
},
},
],
});
};
/**
* 实际发包请求(由 chatBanner 按钮回调触发)。
*
* @param {'gold'|'exp'} type 货币类型
*/
async function doSendRedPacket(type) {
const btn = document.getElementById('red-packet-btn');
if (btn) {
btn.disabled = true;
btn.textContent = '发送中…';
}
try {
const res = await fetch('/command/red-packet/send', {
method: 'POST',
headers: {
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content,
'Content-Type': 'application/json',
'Accept': 'application/json',
},
body: JSON.stringify({
room_id: window.chatContext.roomId,
type
}),
});
const data = await res.json();
if (!res.ok || data.status !== 'success') {
await window.chatDialog.alert(data.message || '发送失败', '操作失败', '#cc4444');
}
// 成功后 WebSocket 广播 RedPacketSent,前端自动弹出红包卡片
} catch (e) {
await window.chatDialog.alert('发送失败:' + e.message, '操作失败', '#cc4444');
} finally {
setTimeout(() => {
if (btn) {
btn.disabled = false;
btn.innerHTML = '🧧 礼包';
}
}, 3000);
}
}
/**
* 展示红包弹窗,并启动倒计时。
*
* @param {number} envelopeId 红包 ID
* @param {string} senderUsername 发包人用户名
* @param {number} totalAmount 总数量
* @param {number} totalCount 总份数
* @param {number} expireSeconds 有效秒数
* @param {'gold'|'exp'} type 货币类型
*/
window.showRedPacketModal = function(envelopeId, senderUsername, totalAmount, totalCount, expireSeconds,
type) {
try {
console.log('showRedPacketModal 触发,当前状态:', {
envelopeId,
senderUsername,
totalAmount,
totalCount,
expireSeconds,
type,
oldId: _rpEnvelopeId
});
_rpEnvelopeId = envelopeId;
_rpClaimed = false;
_rpTotalSeconds = expireSeconds;
_rpExpireAt = Date.now() + expireSeconds * 1000;
_rpType = type || 'gold'; // 保存类型供 claimRedPacket 使用
// 根据类型调整配色和标签
const isExp = (type === 'exp');
const typeIcon = isExp ? '✨' : '💰';
const typeName = isExp ? '经验' : '金币';
const headerBg = isExp ?
'linear-gradient(160deg,#6d28d9 0%,#5b21b6 50%,#4c1d95 100%)' :
'linear-gradient(160deg,#dc2626 0%,#b91c1c 50%,#991b1b 100%)';
const claimBg = isExp ?
'linear-gradient(135deg,#7c3aed,#4f46e5)' :
'linear-gradient(135deg,#dc2626,#ea580c)';
const modalEl = document.getElementById('red-packet-modal');
if (!modalEl) {
alert('致命错误:红包视图容器 #red-packet-modal 找不到!');
return;
}
// 强制解除隐藏,赋予超高权限层级
modalEl.style.setProperty('display', 'flex', 'important');
modalEl.style.setProperty('z-index', '9999999', 'important');
modalEl.style.setProperty('opacity', '1', 'important');
modalEl.style.setProperty('visibility', 'visible', 'important');
// 应用配色
document.getElementById('rp-header').style.background = headerBg;
const claimBtn = document.getElementById('rp-claim-btn');
if (claimBtn) {
claimBtn.style.background = claimBg;
}
// 填入数据
document.getElementById('rp-sender-name').textContent = senderUsername + ' 的礼包';
document.getElementById('rp-total-amount').textContent = totalAmount;
document.getElementById('rp-total-count').textContent = totalCount;
document.getElementById('rp-remaining').textContent = totalCount;
document.getElementById('rp-countdown').textContent = expireSeconds;
document.getElementById('rp-timer-bar').style.width = '100%';
document.getElementById('rp-status-msg').textContent = '';
document.getElementById('rp-claims-list').style.display = 'none';
document.getElementById('rp-claims-items').innerHTML = '';
// 更新卡片标题信息
const emojiEl = modalEl.querySelector('.rp-emoji');
if (emojiEl) emojiEl.textContent = typeIcon;
const titleEl = modalEl.querySelector('.rp-title');
if (titleEl) titleEl.textContent = typeName + '礼包';
const typeLabel = document.getElementById('rp-type-label');
if (typeLabel) typeLabel.textContent = ' ' + typeName;
if (claimBtn) {
claimBtn.disabled = false;
claimBtn.textContent = typeIcon + ' 立即抢包';
}
} catch (err) {
console.error('showRedPacketModal 执行失败:', err);
alert('红包弹窗初始化异常: ' + err.message);
}
// 启动倒计时
clearInterval(_rpTimer);
_rpTimer = setInterval(() => {
const remaining = Math.max(0, Math.ceil((_rpExpireAt - Date.now()) / 1000));
document.getElementById('rp-countdown').textContent = remaining;
document.getElementById('rp-timer-bar').style.width =
(remaining / _rpTotalSeconds * 100) + '%';
if (remaining <= 0) {
clearInterval(_rpTimer);
document.getElementById('rp-claim-btn').disabled = true;
document.getElementById('rp-claim-btn').textContent = '礼包已过期';
document.getElementById('rp-status-msg').style.color = '#9ca3af';
document.getElementById('rp-status-msg').textContent = '红包已过期。';
}
}, 1000);
// 异步拉取服务端最新状态(实时刷新剩余份数)
fetch(`/red-packet/${envelopeId}/status`, {
headers: {
'Accept': 'application/json',
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content,
},
})
.then(r => r.json())
.then(data => {
if (data.status !== 'success') {
return;
}
// 更新剩余份数显示
document.getElementById('rp-remaining').textContent = data.remaining_count;
// 若已过期 → 关闭弹窗 + Toast 提示
if (data.is_expired || data.envelope_status === 'expired') {
clearInterval(_rpTimer);
closeRedPacketModal();
window.chatToast?.show({
title: '⏰ 礼包已过期',
message: '该礼包已超过有效期,无法领取。',
icon: '⏰',
color: '#9ca3af',
duration: 4000,
});
return;
}
// 若已抢完 → 关闭弹窗 + Toast 提示
if (data.remaining_count <= 0 || data.envelope_status === 'completed') {
clearInterval(_rpTimer);
closeRedPacketModal();
window.chatToast?.show({
title: '😅 手慢了!',
message: '礼包已被抢完,下次要快一点哦!',
icon: '🧧',
color: '#f59e0b',
duration: 4000,
});
return;
}
// 若本人已领取 → 关闭弹窗 + Toast 提示
if (data.has_claimed) {
clearInterval(_rpTimer);
closeRedPacketModal();
window.chatToast?.show({
title: '✅ 已领取',
message: '您已成功领取过本次礼包!',
icon: '🧧',
color: '#10b981',
duration: 4000,
});
}
})
.catch(() => {}); // 静默忽略网络错误,不影响弹窗展示
};
// ── 抢包/关闭逻辑 ─────────────────────────────────────
window.closeRedPacketModal = function() {
console.trace('closeRedPacketModal 被调用');
document.getElementById('red-packet-modal').style.display = 'none';
if (_rpTimer) clearInterval(_rpTimer);
};
// 点击遮罩关闭
document.getElementById('red-packet-modal').addEventListener('click', function(e) {
if (e.target === this) {
closeRedPacketModal();
}
});
// ── 抢红包 ──────────────────────────────────────
/**
* 用户点击「立即抢红包」,调用后端 claim 接口。
*/
window.claimRedPacket = async function() {
if (!_rpEnvelopeId) {
return;
}
const btn = document.getElementById('rp-claim-btn');
btn.disabled = true;
btn.textContent = '抢包中…';
try {
const res = await fetch(`/red-packet/${_rpEnvelopeId}/claim`, {
method: 'POST',
headers: {
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content,
'Content-Type': 'application/json',
'Accept': 'application/json',
},
body: JSON.stringify({
room_id: window.chatContext.roomId
}),
});
const data = await res.json();
const statusEl = document.getElementById('rp-status-msg');
const typeLabel = (_rpType === 'exp') ? '经验' : '金币';
if (res.ok && data.status === 'success') {
_rpClaimed = true;
btn.textContent = '🎉 已抢到!';
statusEl.style.color = '#16a34a';
statusEl.textContent = `恭喜!您抢到了 ${data.amount} ${typeLabel}`;
// 弹出全局 Toast
window.chatToast.show({
title: '🧧 礼包到账',
message: `恭喜您抢到了礼包 <b>${data.amount}</b> ${typeLabel}`,
icon: '🧧',
color: (_rpType === 'exp') ? '#7c3aed' : '#dc2626',
duration: 8000,
});
// 3 秒后自动关闭弹窗
setTimeout(() => closeRedPacketModal(), 3000);
} else {
statusEl.style.color = '#dc2626';
statusEl.textContent = data.message || '抢包失败';
// 若已领过或已抢完则禁用按钮,否则解除禁用以重试
if (data.message && (data.message.includes('已经领过') || data.message.includes('已被抢完') ||
data.message.includes('已抢完'))) {
btn.textContent = '已参与';
} else {
btn.disabled = false;
btn.textContent = '🧧 立即抢红包';
}
}
} catch (e) {
document.getElementById('rp-status-msg').textContent = '网络异常,请重试';
document.getElementById('rp-status-msg').style.color = '#dc2626';
btn.disabled = false;
btn.textContent = '🧧 立即抢红包';
}
};
// ── 更新领取名单(被 WS 触发调用)───────────────
/**
* 收到「系统传音」中的领取播报时,同步更新弹窗内的名单与剩余数。
*
* @param {string} username 领取者用户名
* @param {number} amount 领取金额
* @param {number} remaining 剩余份数
*/
window.updateRedPacketClaimsUI = function(username, amount, remaining) {
const remainingEl = document.getElementById('rp-remaining');
if (remainingEl) {
remainingEl.textContent = remaining;
}
const listEl = document.getElementById('rp-claims-list');
const itemsEl = document.getElementById('rp-claims-items');
if (!listEl || !itemsEl) {
return;
}
listEl.style.display = 'block';
const item = document.createElement('div');
item.className = 'rp-claim-item';
item.innerHTML = `<span>${username}</span><span>+${amount} 金币</span>`;
itemsEl.prepend(item);
// 若已全部领完,更新按钮状态
if (remaining <= 0) {
const btn = document.getElementById('rp-claim-btn');
if (btn && !_rpClaimed) {
btn.disabled = true;
btn.textContent = '礼包已被抢完!';
}
clearInterval(_rpTimer);
// 3 秒后自动关闭
setTimeout(() => closeRedPacketModal(), 3000);
}
};
// ── WebSocket 监听 red-packet.sent ───────────────
/**
* 等待 Echo 就绪后注册 red-packet.sent 事件监听,
* 每次收到新红包时弹出红包卡片弹窗。
*/
function setupRedPacketListener() {
if (!window.Echo || !window.chatContext) {
setTimeout(setupRedPacketListener, 500);
return;
}
window.Echo.join(`room.${window.chatContext.roomId}`)
.listen('.red-packet.sent', (e) => {
// 收到红包事件,弹出卡片(type 决定金币/经验配色)
showRedPacketModal(
e.envelope_id,
e.sender_username,
e.total_amount,
e.total_count,
e.expire_seconds,
e.type || 'gold',
);
});
console.log('RedPacketSent 监听器已注册');
}
document.addEventListener('DOMContentLoaded', setupRedPacketListener);
})(); // end IIFE
</script>