迁移礼包红包脚本

This commit is contained in:
2026-04-25 18:22:27 +08:00
parent a5c43383e1
commit 8e57ad8a45
3 changed files with 709 additions and 522 deletions
@@ -270,525 +270,4 @@
</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 = async function() {
const btn = document.getElementById('red-packet-btn');
if (btn) {
btn.disabled = true;
btn.textContent = '读取中…';
}
try {
const config = await fetchRedPacketConfig();
const amountText = Number(config.amount || 0).toLocaleString('zh-CN');
const countText = Number(config.count || 0).toLocaleString('zh-CN');
window.chatBanner.show({
icon: '🧧',
title: '发出礼包',
name: '选择礼包类型',
body: `将发出 ${amountText} 数量共 ${countText} 份的礼包,系统凭空发放,房间成员先到先得!`,
gradient: ['#991b1b', '#dc2626', '#ea580c'],
titleColor: '#fde68a',
autoClose: 0,
buttons: [{
label: '💰 金币礼包',
color: '#d97706',
onClick(button, close) {
close();
doSendRedPacket('gold');
},
},
{
label: '✨ 经验礼包',
color: '#7c3aed',
onClick(button, close) {
close();
doSendRedPacket('exp');
},
},
{
label: '取消',
color: 'rgba(255,255,255,0.15)',
onClick(button, close) {
close();
},
},
],
});
} catch (e) {
await window.chatDialog.alert(e.message || '读取礼包配置失败', '操作失败', '#cc4444');
} finally {
if (btn) {
btn.disabled = false;
btn.innerHTML = '🧧 礼包';
}
}
};
/**
* 读取当前职务的礼包红包默认配置。
*
* @returns {Promise<{amount:number,count:number,expire_seconds:number}>}
*/
async function fetchRedPacketConfig() {
const res = await fetch('/command/red-packet/config', {
headers: {
'Accept': 'application/json',
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content,
},
});
const data = await res.json();
if (!res.ok || data.status !== 'success') {
throw new Error(data.message || '读取礼包配置失败');
}
return data;
}
/**
* 实际发包请求(由 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();
}
});
// 静态按钮统一由脚本绑定,避免红包弹窗 DOM 中继续保留内联 onclick。
document.getElementById('rp-close-btn')?.addEventListener('click', function(e) {
e.preventDefault();
closeRedPacketModal();
});
document.getElementById('rp-claim-btn')?.addEventListener('click', function(e) {
e.preventDefault();
void claimRedPacket();
});
// ── 抢红包 ──────────────────────────────────────
/**
* 用户点击「立即抢红包」,调用后端 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: `恭喜您抢到了礼包 ${data.amount} ${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>
{{-- 礼包红包前端交互脚本已迁移到 resources/js/chat-room/red-packet-panel.js --}}