Files
2026-04-21 15:10:41 +08:00

744 lines
28 KiB
PHP
Raw Permalink 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 / red-packet.claimed 广播事件
*/
(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>8888</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 = async function(envelopeId, senderUsername, totalAmount, totalCount,
expireSeconds,
type) {
try {
// 尝试获取点击按钮附带的发包真实时间戳(兼容历史数据)
let sentAtUnix = null;
if (window.event && window.event.currentTarget) {
const btn = window.event.currentTarget;
if (btn.dataset && btn.dataset.sentAt) {
sentAtUnix = parseInt(btn.dataset.sentAt, 10);
}
}
console.log('showRedPacketModal 触发,当前状态:', {
envelopeId,
senderUsername,
totalAmount,
totalCount,
expireSeconds,
type,
sentAtUnix,
oldId: _rpEnvelopeId
});
// 计算真实过期时间点
let calculatedExpireAt = Date.now() + expireSeconds * 1000;
if (sentAtUnix && !isNaN(sentAtUnix) && sentAtUnix > 0) {
calculatedExpireAt = (sentAtUnix + expireSeconds) * 1000;
}
// 【前置拦截1】如果有时间戳并算出已过期,直接杀死不弹窗
if (sentAtUnix && Date.now() >= calculatedExpireAt) {
window.chatToast?.show({
title: '⏰ 礼包已过期',
message: '该红包已过期,无法领取。',
icon: '⏰',
color: '#9ca3af',
duration: 4000,
});
console.log('红包已准确断定过期,拦截弹窗显示:', envelopeId);
return;
}
// 【统一前置拦截】无论新老红包、有无时间戳,为彻底杜绝闪现,强制上云查册生死再放行!
let currentRemaining = totalCount;
try {
const res = await fetch(`/red-packet/${envelopeId}/status`, {
headers: {
'Accept': 'application/json',
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]')
.content,
},
});
const initialStatusData = await res.json();
if (initialStatusData.status === 'success') {
if (initialStatusData.is_expired || initialStatusData.envelope_status ===
'expired') {
window.chatToast?.show({
title: '⏰ 礼包已过期',
message: '该红包已过期,无法领取。',
icon: '⏰',
color: '#9ca3af',
duration: 4000,
});
return; // 判定死亡,直接退出,永不渲染弹窗!
}
if (initialStatusData.remaining_count <= 0 || initialStatusData
.envelope_status === 'completed') {
window.chatToast?.show({
title: '😅 手慢了!',
message: '红包已被抢完,下次要快一点哦!',
icon: '🧧',
color: '#f59e0b',
duration: 4000,
});
return;
}
if (initialStatusData.has_claimed) {
window.chatToast?.show({
title: '✅ 已领取',
message: '您已成功领取过本次礼包!',
icon: '🧧',
color: '#10b981',
duration: 4000,
});
return; // 判定已领取,直接退出
}
// 记录真实的剩余倒计时以备展示
currentRemaining = initialStatusData.remaining_count;
totalCount = initialStatusData.total_count || totalCount;
}
} catch (e) {
console.error('红包状态前置预查失败:', e);
}
// --------- 到此,证实它不仅没死甚至还很活泼、也没领取过。开始安心布置并渲染弹窗 ---------
_rpEnvelopeId = envelopeId;
_rpClaimed = false;
_rpType = type || 'gold';
_rpExpireAt = calculatedExpireAt;
_rpTotalSeconds = expireSeconds;
// 根据类型调整配色和标签
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) {
window.chatDialog?.alert('致命错误:红包视图容器 #red-packet-modal 找不到!', '系统错误', '#cc4444');
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 = currentRemaining;
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);
window.chatDialog?.alert('红包弹窗初始化异常: ' + err.message, '系统错误', '#cc4444');
}
// 启动倒计时
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 = '红包已过期,即将关闭…';
// 3 秒后自动关闭弹窗
setTimeout(() => closeRedPacketModal(), 3000);
}
}, 1000);
};
// ── 抢包/关闭逻辑 ─────────────────────────────────────
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}`;
const remainingEl = document.getElementById('rp-remaining');
if (remainingEl && typeof data.remaining_count === 'number') {
remainingEl.textContent = data.remaining_count;
}
// 弹出全局 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 || '抢包失败';
const message = data.message || '';
const shouldAutoClose = message.includes('已过期')
|| message.includes('已被抢完')
|| message.includes('已抢完')
|| message.includes('红包已抢完或已过期');
// 若红包已经结束,则保持禁用并在 3 秒后自动关闭弹窗。
if (shouldAutoClose) {
btn.textContent = '礼包已结束';
setTimeout(() => closeRedPacketModal(), 3000);
} else if (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 剩余份数
* @param {'gold'|'exp'} [type] 红包类型
*/
window.updateRedPacketClaimsUI = function(username, amount, remaining, type = _rpType) {
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 typeLabel = type === 'exp' ? '经验' : '金币';
const item = document.createElement('div');
item.className = 'rp-claim-item';
item.innerHTML = `<span>${username}</span><span>+${amount} ${typeLabel}</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 / red-packet.claimed ───────────────
/**
* 等待 Echo 就绪后注册红包相关事件监听,
* 新红包弹窗展示,领取成功后实时刷新剩余份数。
*/
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',
);
})
.listen('.red-packet.claimed', (e) => {
if (Number(e.envelope_id) !== Number(_rpEnvelopeId)) {
return;
}
window.updateRedPacketClaimsUI(
e.claimer_username,
e.amount,
e.remaining_count,
e.type || _rpType,
);
});
console.log('RedPacketSent 监听器已注册');
}
document.addEventListener('DOMContentLoaded', setupRedPacketListener);
})(); // end IIFE
</script>