迁移礼包红包脚本
This commit is contained in:
@@ -46,6 +46,7 @@
|
|||||||
* - right-panel.js:处理右侧在线用户列表和用户名交互。
|
* - right-panel.js:处理右侧在线用户列表和用户名交互。
|
||||||
* - rooms.js:处理房间在线状态渲染和跳转 URL。
|
* - rooms.js:处理房间在线状态渲染和跳转 URL。
|
||||||
* - reward-modal.js:处理职务奖励金币弹窗入口。
|
* - reward-modal.js:处理职务奖励金币弹窗入口。
|
||||||
|
* - red-packet-panel.js:处理礼包红包发包、抢包、倒计时和广播监听。
|
||||||
* - message-queue.js:提供聊天消息分批渲染队列。
|
* - message-queue.js:提供聊天消息分批渲染队列。
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -196,6 +197,14 @@ export {
|
|||||||
resolveRoomUrl,
|
resolveRoomUrl,
|
||||||
} from "./chat-room/rooms.js";
|
} from "./chat-room/rooms.js";
|
||||||
export { bindRewardModalControls, openRewardModal } from "./chat-room/reward-modal.js";
|
export { bindRewardModalControls, openRewardModal } from "./chat-room/reward-modal.js";
|
||||||
|
export {
|
||||||
|
bindRedPacketPanelControls,
|
||||||
|
claimRedPacket,
|
||||||
|
closeRedPacketModal,
|
||||||
|
sendRedPacket,
|
||||||
|
showRedPacketModal,
|
||||||
|
updateRedPacketClaimsUI,
|
||||||
|
} from "./chat-room/red-packet-panel.js";
|
||||||
export { createMessageQueue } from "./chat-room/message-queue.js";
|
export { createMessageQueue } from "./chat-room/message-queue.js";
|
||||||
|
|
||||||
import { escapeHtml, escapeHtmlWithLineBreaks } from "./chat-room/html.js";
|
import { escapeHtml, escapeHtmlWithLineBreaks } from "./chat-room/html.js";
|
||||||
@@ -344,6 +353,14 @@ import {
|
|||||||
resolveRoomUrl,
|
resolveRoomUrl,
|
||||||
} from "./chat-room/rooms.js";
|
} from "./chat-room/rooms.js";
|
||||||
import { bindRewardModalControls, openRewardModal } from "./chat-room/reward-modal.js";
|
import { bindRewardModalControls, openRewardModal } from "./chat-room/reward-modal.js";
|
||||||
|
import {
|
||||||
|
bindRedPacketPanelControls,
|
||||||
|
claimRedPacket,
|
||||||
|
closeRedPacketModal,
|
||||||
|
sendRedPacket,
|
||||||
|
showRedPacketModal,
|
||||||
|
updateRedPacketClaimsUI,
|
||||||
|
} from "./chat-room/red-packet-panel.js";
|
||||||
import { createMessageQueue } from "./chat-room/message-queue.js";
|
import { createMessageQueue } from "./chat-room/message-queue.js";
|
||||||
|
|
||||||
if (typeof window !== "undefined") {
|
if (typeof window !== "undefined") {
|
||||||
@@ -524,6 +541,12 @@ if (typeof window !== "undefined") {
|
|||||||
resolveRoomUrl,
|
resolveRoomUrl,
|
||||||
bindRewardModalControls,
|
bindRewardModalControls,
|
||||||
openRewardModal,
|
openRewardModal,
|
||||||
|
bindRedPacketPanelControls,
|
||||||
|
claimRedPacket,
|
||||||
|
closeRedPacketModal,
|
||||||
|
sendRedPacket,
|
||||||
|
showRedPacketModal,
|
||||||
|
updateRedPacketClaimsUI,
|
||||||
createMessageQueue,
|
createMessageQueue,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -598,6 +621,11 @@ if (typeof window !== "undefined") {
|
|||||||
window.switchVipTab = switchVipTab;
|
window.switchVipTab = switchVipTab;
|
||||||
window.switchBankTab = switchBankTab;
|
window.switchBankTab = switchBankTab;
|
||||||
window.toggleBankRankSort = toggleBankRankSort;
|
window.toggleBankRankSort = toggleBankRankSort;
|
||||||
|
window.claimRedPacket = claimRedPacket;
|
||||||
|
window.closeRedPacketModal = closeRedPacketModal;
|
||||||
|
window.sendRedPacket = sendRedPacket;
|
||||||
|
window.showRedPacketModal = showRedPacketModal;
|
||||||
|
window.updateRedPacketClaimsUI = updateRedPacketClaimsUI;
|
||||||
window.applyFontSize = applyFontSize;
|
window.applyFontSize = applyFontSize;
|
||||||
window.closeAvatarPicker = closeAvatarPicker;
|
window.closeAvatarPicker = closeAvatarPicker;
|
||||||
window.closeSettingsModal = closeSettingsModal;
|
window.closeSettingsModal = closeSettingsModal;
|
||||||
@@ -677,6 +705,7 @@ if (typeof window !== "undefined") {
|
|||||||
bindChatRightPanelControls();
|
bindChatRightPanelControls();
|
||||||
bindRoomStatusControls();
|
bindRoomStatusControls();
|
||||||
bindRewardModalControls();
|
bindRewardModalControls();
|
||||||
|
bindRedPacketPanelControls();
|
||||||
bindMobileDrawerControls();
|
bindMobileDrawerControls();
|
||||||
bindWelcomeMenuControls();
|
bindWelcomeMenuControls();
|
||||||
bindBlockMenuControls();
|
bindBlockMenuControls();
|
||||||
|
|||||||
@@ -0,0 +1,679 @@
|
|||||||
|
// 礼包红包前端交互模块,负责发包、抢包、弹窗倒计时和广播监听。
|
||||||
|
|
||||||
|
let redPacketEnvelopeId = null;
|
||||||
|
let redPacketExpireAt = null;
|
||||||
|
let redPacketTotalSeconds = 120;
|
||||||
|
let redPacketTimer = null;
|
||||||
|
let redPacketClaimed = false;
|
||||||
|
let redPacketType = "gold";
|
||||||
|
let redPacketEventsBound = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 读取 CSRF Token,给红包接口请求统一使用。
|
||||||
|
*
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
function csrfToken() {
|
||||||
|
return document.querySelector('meta[name="csrf-token"]')?.content ?? "";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 读取指定 DOM 节点。
|
||||||
|
*
|
||||||
|
* @param {string} id 节点 ID
|
||||||
|
* @returns {HTMLElement|null}
|
||||||
|
*/
|
||||||
|
function byId(id) {
|
||||||
|
return document.getElementById(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重置礼包按钮状态。
|
||||||
|
*
|
||||||
|
* @param {HTMLButtonElement|null} button 礼包按钮
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
function resetRedPacketButton(button) {
|
||||||
|
if (!button) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.disabled = false;
|
||||||
|
button.innerHTML = "🧧 礼包";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 读取当前职务的礼包红包默认配置。
|
||||||
|
*
|
||||||
|
* @returns {Promise<{amount:number,count:number,expire_seconds:number}>}
|
||||||
|
*/
|
||||||
|
async function fetchRedPacketConfig() {
|
||||||
|
const response = await fetch("/command/red-packet/config", {
|
||||||
|
headers: {
|
||||||
|
Accept: "application/json",
|
||||||
|
"X-CSRF-TOKEN": csrfToken(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (!response.ok || data.status !== "success") {
|
||||||
|
throw new Error(data.message || "读取礼包配置失败");
|
||||||
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发起实际发包请求。
|
||||||
|
*
|
||||||
|
* @param {"gold"|"exp"} type 礼包类型
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
async function doSendRedPacket(type) {
|
||||||
|
const button = byId("red-packet-btn");
|
||||||
|
|
||||||
|
if (button) {
|
||||||
|
button.disabled = true;
|
||||||
|
button.textContent = "发送中…";
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch("/command/red-packet/send", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"X-CSRF-TOKEN": csrfToken(),
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Accept: "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
room_id: window.chatContext.roomId,
|
||||||
|
type,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (!response.ok || data.status !== "success") {
|
||||||
|
await window.chatDialog.alert(data.message || "发送失败", "操作失败", "#cc4444");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
await window.chatDialog.alert(`发送失败:${error.message}`, "操作失败", "#cc4444");
|
||||||
|
} finally {
|
||||||
|
setTimeout(() => resetRedPacketButton(button), 3000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* superlevel 点击“礼包”按钮后弹出类型选择。
|
||||||
|
*
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
export async function sendRedPacket() {
|
||||||
|
const button = byId("red-packet-btn");
|
||||||
|
|
||||||
|
if (button) {
|
||||||
|
button.disabled = true;
|
||||||
|
button.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(buttonEl, close) {
|
||||||
|
close();
|
||||||
|
doSendRedPacket("gold");
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "✨ 经验礼包",
|
||||||
|
color: "#7c3aed",
|
||||||
|
onClick(buttonEl, close) {
|
||||||
|
close();
|
||||||
|
doSendRedPacket("exp");
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "取消",
|
||||||
|
color: "rgba(255,255,255,0.15)",
|
||||||
|
onClick(buttonEl, close) {
|
||||||
|
close();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
await window.chatDialog.alert(error.message || "读取礼包配置失败", "操作失败", "#cc4444");
|
||||||
|
} finally {
|
||||||
|
resetRedPacketButton(button);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从触发按钮读取红包发送时间,兼容历史系统消息中的 data-sent-at。
|
||||||
|
*
|
||||||
|
* @returns {number|null}
|
||||||
|
*/
|
||||||
|
function readSentAtFromCurrentEvent() {
|
||||||
|
if (!window.event?.currentTarget?.dataset?.sentAt) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sentAt = Number.parseInt(window.event.currentTarget.dataset.sentAt, 10);
|
||||||
|
|
||||||
|
return Number.isNaN(sentAt) ? null : sentAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 预查红包状态,避免已过期或抢完的红包弹窗闪现。
|
||||||
|
*
|
||||||
|
* @param {number|string} envelopeId 红包 ID
|
||||||
|
* @param {number} totalCount 总份数
|
||||||
|
* @returns {Promise<{allowed:boolean, remainingCount:number, totalCount:number}>}
|
||||||
|
*/
|
||||||
|
async function preflightRedPacketStatus(envelopeId, totalCount) {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/red-packet/${envelopeId}/status`, {
|
||||||
|
headers: {
|
||||||
|
Accept: "application/json",
|
||||||
|
"X-CSRF-TOKEN": csrfToken(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.status !== "success") {
|
||||||
|
return {
|
||||||
|
allowed: true,
|
||||||
|
remainingCount: totalCount,
|
||||||
|
totalCount,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.is_expired || data.envelope_status === "expired") {
|
||||||
|
window.chatToast?.show({
|
||||||
|
title: "⏰ 礼包已过期",
|
||||||
|
message: "该红包已过期,无法领取。",
|
||||||
|
icon: "⏰",
|
||||||
|
color: "#9ca3af",
|
||||||
|
duration: 4000,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
allowed: false,
|
||||||
|
remainingCount: 0,
|
||||||
|
totalCount,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.remaining_count <= 0 || data.envelope_status === "completed") {
|
||||||
|
window.chatToast?.show({
|
||||||
|
title: "😅 手慢了!",
|
||||||
|
message: "红包已被抢完,下次要快一点哦!",
|
||||||
|
icon: "🧧",
|
||||||
|
color: "#f59e0b",
|
||||||
|
duration: 4000,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
allowed: false,
|
||||||
|
remainingCount: 0,
|
||||||
|
totalCount,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.has_claimed) {
|
||||||
|
window.chatToast?.show({
|
||||||
|
title: "✅ 已领取",
|
||||||
|
message: "您已成功领取过本次礼包!",
|
||||||
|
icon: "🧧",
|
||||||
|
color: "#10b981",
|
||||||
|
duration: 4000,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
allowed: false,
|
||||||
|
remainingCount: data.remaining_count,
|
||||||
|
totalCount: data.total_count || totalCount,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
allowed: true,
|
||||||
|
remainingCount: data.remaining_count,
|
||||||
|
totalCount: data.total_count || totalCount,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error("红包状态前置预查失败:", error);
|
||||||
|
|
||||||
|
return {
|
||||||
|
allowed: true,
|
||||||
|
remainingCount: totalCount,
|
||||||
|
totalCount,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 应用红包弹窗的类型配色。
|
||||||
|
*
|
||||||
|
* @param {"gold"|"exp"} type 礼包类型
|
||||||
|
* @returns {{typeIcon:string,typeName:string}}
|
||||||
|
*/
|
||||||
|
function applyRedPacketTypeStyle(type) {
|
||||||
|
const isExp = type === "exp";
|
||||||
|
const typeIcon = isExp ? "✨" : "💰";
|
||||||
|
const typeName = isExp ? "经验" : "金币";
|
||||||
|
const headerBackground = isExp
|
||||||
|
? "linear-gradient(160deg,#6d28d9 0%,#5b21b6 50%,#4c1d95 100%)"
|
||||||
|
: "linear-gradient(160deg,#dc2626 0%,#b91c1c 50%,#991b1b 100%)";
|
||||||
|
const claimBackground = isExp
|
||||||
|
? "linear-gradient(135deg,#7c3aed,#4f46e5)"
|
||||||
|
: "linear-gradient(135deg,#dc2626,#ea580c)";
|
||||||
|
|
||||||
|
byId("rp-header").style.background = headerBackground;
|
||||||
|
|
||||||
|
const claimButton = byId("rp-claim-btn");
|
||||||
|
if (claimButton) {
|
||||||
|
claimButton.style.background = claimBackground;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
typeIcon,
|
||||||
|
typeName,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 填充红包弹窗内容。
|
||||||
|
*
|
||||||
|
* @param {Record<string, any>} payload 弹窗数据
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
function renderRedPacketModal(payload) {
|
||||||
|
const modal = byId("red-packet-modal");
|
||||||
|
const claimButton = byId("rp-claim-btn");
|
||||||
|
const styleConfig = applyRedPacketTypeStyle(payload.type);
|
||||||
|
|
||||||
|
modal.style.setProperty("display", "flex", "important");
|
||||||
|
modal.style.setProperty("z-index", "9999999", "important");
|
||||||
|
modal.style.setProperty("opacity", "1", "important");
|
||||||
|
modal.style.setProperty("visibility", "visible", "important");
|
||||||
|
|
||||||
|
byId("rp-sender-name").textContent = `${payload.senderUsername} 的礼包`;
|
||||||
|
byId("rp-total-amount").textContent = payload.totalAmount;
|
||||||
|
byId("rp-total-count").textContent = payload.totalCount;
|
||||||
|
byId("rp-remaining").textContent = payload.remainingCount;
|
||||||
|
byId("rp-countdown").textContent = payload.expireSeconds;
|
||||||
|
byId("rp-timer-bar").style.width = "100%";
|
||||||
|
byId("rp-status-msg").textContent = "";
|
||||||
|
byId("rp-claims-list").style.display = "none";
|
||||||
|
byId("rp-claims-items").textContent = "";
|
||||||
|
|
||||||
|
const emoji = modal.querySelector(".rp-emoji");
|
||||||
|
if (emoji) {
|
||||||
|
emoji.textContent = styleConfig.typeIcon;
|
||||||
|
}
|
||||||
|
|
||||||
|
const title = modal.querySelector(".rp-title");
|
||||||
|
if (title) {
|
||||||
|
title.textContent = `${styleConfig.typeName}礼包`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const typeLabel = byId("rp-type-label");
|
||||||
|
if (typeLabel) {
|
||||||
|
typeLabel.textContent = ` ${styleConfig.typeName}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (claimButton) {
|
||||||
|
claimButton.disabled = false;
|
||||||
|
claimButton.textContent = `${styleConfig.typeIcon} 立即抢包`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 启动红包倒计时。
|
||||||
|
*
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
function startRedPacketTimer() {
|
||||||
|
clearInterval(redPacketTimer);
|
||||||
|
redPacketTimer = setInterval(() => {
|
||||||
|
const remaining = Math.max(0, Math.ceil((redPacketExpireAt - Date.now()) / 1000));
|
||||||
|
byId("rp-countdown").textContent = remaining;
|
||||||
|
byId("rp-timer-bar").style.width = `${(remaining / redPacketTotalSeconds) * 100}%`;
|
||||||
|
|
||||||
|
if (remaining <= 0) {
|
||||||
|
clearInterval(redPacketTimer);
|
||||||
|
|
||||||
|
const claimButton = byId("rp-claim-btn");
|
||||||
|
claimButton.disabled = true;
|
||||||
|
claimButton.textContent = "礼包已过期";
|
||||||
|
|
||||||
|
const status = byId("rp-status-msg");
|
||||||
|
status.style.color = "#9ca3af";
|
||||||
|
status.textContent = "红包已过期,即将关闭…";
|
||||||
|
|
||||||
|
setTimeout(() => closeRedPacketModal(), 3000);
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 展示红包弹窗,并启动倒计时。
|
||||||
|
*
|
||||||
|
* @param {number|string} envelopeId 红包 ID
|
||||||
|
* @param {string} senderUsername 发包人
|
||||||
|
* @param {number|string} totalAmount 总数量
|
||||||
|
* @param {number} totalCount 总份数
|
||||||
|
* @param {number} expireSeconds 有效秒数
|
||||||
|
* @param {"gold"|"exp"} type 礼包类型
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
export async function showRedPacketModal(envelopeId, senderUsername, totalAmount, totalCount, expireSeconds, type = "gold") {
|
||||||
|
try {
|
||||||
|
const sentAt = readSentAtFromCurrentEvent();
|
||||||
|
let calculatedExpireAt = Date.now() + expireSeconds * 1000;
|
||||||
|
|
||||||
|
if (sentAt && sentAt > 0) {
|
||||||
|
calculatedExpireAt = (sentAt + expireSeconds) * 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sentAt && Date.now() >= calculatedExpireAt) {
|
||||||
|
window.chatToast?.show({
|
||||||
|
title: "⏰ 礼包已过期",
|
||||||
|
message: "该红包已过期,无法领取。",
|
||||||
|
icon: "⏰",
|
||||||
|
color: "#9ca3af",
|
||||||
|
duration: 4000,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const preflight = await preflightRedPacketStatus(envelopeId, totalCount);
|
||||||
|
if (!preflight.allowed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
redPacketEnvelopeId = envelopeId;
|
||||||
|
redPacketClaimed = false;
|
||||||
|
redPacketType = type || "gold";
|
||||||
|
redPacketExpireAt = calculatedExpireAt;
|
||||||
|
redPacketTotalSeconds = expireSeconds;
|
||||||
|
|
||||||
|
const modal = byId("red-packet-modal");
|
||||||
|
if (!modal) {
|
||||||
|
window.chatDialog?.alert("致命错误:红包视图容器 #red-packet-modal 找不到!", "系统错误", "#cc4444");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
renderRedPacketModal({
|
||||||
|
senderUsername,
|
||||||
|
totalAmount,
|
||||||
|
totalCount: preflight.totalCount,
|
||||||
|
remainingCount: preflight.remainingCount,
|
||||||
|
expireSeconds,
|
||||||
|
type: redPacketType,
|
||||||
|
});
|
||||||
|
startRedPacketTimer();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("showRedPacketModal 执行失败:", error);
|
||||||
|
window.chatDialog?.alert(`红包弹窗初始化异常: ${error.message}`, "系统错误", "#cc4444");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 关闭红包弹窗并停止倒计时。
|
||||||
|
*
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
export function closeRedPacketModal() {
|
||||||
|
const modal = byId("red-packet-modal");
|
||||||
|
|
||||||
|
if (modal) {
|
||||||
|
modal.style.display = "none";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (redPacketTimer) {
|
||||||
|
clearInterval(redPacketTimer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户点击“立即抢红包”。
|
||||||
|
*
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
export async function claimRedPacket() {
|
||||||
|
if (!redPacketEnvelopeId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const button = byId("rp-claim-btn");
|
||||||
|
button.disabled = true;
|
||||||
|
button.textContent = "抢包中…";
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/red-packet/${redPacketEnvelopeId}/claim`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"X-CSRF-TOKEN": csrfToken(),
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Accept: "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
room_id: window.chatContext.roomId,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
const data = await response.json();
|
||||||
|
handleClaimResponse(response, data, button);
|
||||||
|
} catch (error) {
|
||||||
|
const status = byId("rp-status-msg");
|
||||||
|
status.textContent = "网络异常,请重试";
|
||||||
|
status.style.color = "#dc2626";
|
||||||
|
button.disabled = false;
|
||||||
|
button.textContent = "🧧 立即抢红包";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理抢红包接口响应。
|
||||||
|
*
|
||||||
|
* @param {Response} response Fetch 响应对象
|
||||||
|
* @param {Record<string, any>} data 接口数据
|
||||||
|
* @param {HTMLButtonElement} button 抢包按钮
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
function handleClaimResponse(response, data, button) {
|
||||||
|
const status = byId("rp-status-msg");
|
||||||
|
const typeLabel = redPacketType === "exp" ? "经验" : "金币";
|
||||||
|
|
||||||
|
if (response.ok && data.status === "success") {
|
||||||
|
redPacketClaimed = true;
|
||||||
|
button.textContent = "🎉 已抢到!";
|
||||||
|
status.style.color = "#16a34a";
|
||||||
|
status.textContent = `恭喜!您抢到了 ${data.amount} ${typeLabel}!`;
|
||||||
|
|
||||||
|
const remaining = byId("rp-remaining");
|
||||||
|
if (remaining && typeof data.remaining_count === "number") {
|
||||||
|
remaining.textContent = data.remaining_count;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.chatToast.show({
|
||||||
|
title: "🧧 礼包到账",
|
||||||
|
message: `恭喜您抢到了礼包 ${data.amount} ${typeLabel}!`,
|
||||||
|
icon: "🧧",
|
||||||
|
color: redPacketType === "exp" ? "#7c3aed" : "#dc2626",
|
||||||
|
duration: 8000,
|
||||||
|
});
|
||||||
|
|
||||||
|
setTimeout(() => closeRedPacketModal(), 3000);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
status.style.color = "#dc2626";
|
||||||
|
status.textContent = data.message || "抢包失败";
|
||||||
|
updateClaimButtonAfterFailure(button, data.message || "");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据失败原因更新抢包按钮状态。
|
||||||
|
*
|
||||||
|
* @param {HTMLButtonElement} button 抢包按钮
|
||||||
|
* @param {string} message 失败文案
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
function updateClaimButtonAfterFailure(button, message) {
|
||||||
|
const shouldAutoClose = message.includes("已过期")
|
||||||
|
|| message.includes("已被抢完")
|
||||||
|
|| message.includes("已抢完")
|
||||||
|
|| message.includes("红包已抢完或已过期");
|
||||||
|
|
||||||
|
if (shouldAutoClose) {
|
||||||
|
button.textContent = "礼包已结束";
|
||||||
|
setTimeout(() => closeRedPacketModal(), 3000);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message.includes("已经领过")) {
|
||||||
|
button.textContent = "已参与";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.disabled = false;
|
||||||
|
button.textContent = "🧧 立即抢红包";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 收到领取广播后,同步弹窗内领取名单与剩余数。
|
||||||
|
*
|
||||||
|
* @param {string} username 领取者用户名
|
||||||
|
* @param {number|string} amount 领取数量
|
||||||
|
* @param {number} remaining 剩余份数
|
||||||
|
* @param {"gold"|"exp"} type 礼包类型
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
export function updateRedPacketClaimsUI(username, amount, remaining, type = redPacketType) {
|
||||||
|
const remainingElement = byId("rp-remaining");
|
||||||
|
if (remainingElement) {
|
||||||
|
remainingElement.textContent = remaining;
|
||||||
|
}
|
||||||
|
|
||||||
|
const list = byId("rp-claims-list");
|
||||||
|
const items = byId("rp-claims-items");
|
||||||
|
if (!list || !items) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
list.style.display = "block";
|
||||||
|
|
||||||
|
const item = document.createElement("div");
|
||||||
|
const name = document.createElement("span");
|
||||||
|
const value = document.createElement("span");
|
||||||
|
const typeLabel = type === "exp" ? "经验" : "金币";
|
||||||
|
|
||||||
|
item.className = "rp-claim-item";
|
||||||
|
name.textContent = username;
|
||||||
|
value.textContent = `+${amount} ${typeLabel}`;
|
||||||
|
item.append(name, value);
|
||||||
|
items.prepend(item);
|
||||||
|
|
||||||
|
if (remaining <= 0) {
|
||||||
|
const button = byId("rp-claim-btn");
|
||||||
|
if (button && !redPacketClaimed) {
|
||||||
|
button.disabled = true;
|
||||||
|
button.textContent = "礼包已被抢完!";
|
||||||
|
}
|
||||||
|
|
||||||
|
clearInterval(redPacketTimer);
|
||||||
|
setTimeout(() => closeRedPacketModal(), 3000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 注册红包 Echo 监听。
|
||||||
|
*
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
function setupRedPacketListener() {
|
||||||
|
if (!window.Echo || !window.chatContext) {
|
||||||
|
setTimeout(setupRedPacketListener, 500);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.Echo.join(`room.${window.chatContext.roomId}`)
|
||||||
|
.listen(".red-packet.sent", (event) => {
|
||||||
|
showRedPacketModal(
|
||||||
|
event.envelope_id,
|
||||||
|
event.sender_username,
|
||||||
|
event.total_amount,
|
||||||
|
event.total_count,
|
||||||
|
event.expire_seconds,
|
||||||
|
event.type || "gold",
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.listen(".red-packet.claimed", (event) => {
|
||||||
|
if (Number(event.envelope_id) !== Number(redPacketEnvelopeId)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateRedPacketClaimsUI(
|
||||||
|
event.claimer_username,
|
||||||
|
event.amount,
|
||||||
|
event.remaining_count,
|
||||||
|
event.type || redPacketType,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 挂载红包全局入口并绑定静态按钮事件。
|
||||||
|
*
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
export function bindRedPacketPanelControls() {
|
||||||
|
if (typeof window === "undefined" || typeof document === "undefined") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.sendRedPacket = sendRedPacket;
|
||||||
|
window.showRedPacketModal = showRedPacketModal;
|
||||||
|
window.closeRedPacketModal = closeRedPacketModal;
|
||||||
|
window.claimRedPacket = claimRedPacket;
|
||||||
|
window.updateRedPacketClaimsUI = updateRedPacketClaimsUI;
|
||||||
|
|
||||||
|
if (redPacketEventsBound) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
redPacketEventsBound = true;
|
||||||
|
|
||||||
|
byId("red-packet-modal")?.addEventListener("click", (event) => {
|
||||||
|
if (event.target === event.currentTarget) {
|
||||||
|
closeRedPacketModal();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
byId("rp-close-btn")?.addEventListener("click", (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
closeRedPacketModal();
|
||||||
|
});
|
||||||
|
|
||||||
|
byId("rp-claim-btn")?.addEventListener("click", (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
claimRedPacket();
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener("DOMContentLoaded", setupRedPacketListener);
|
||||||
|
}
|
||||||
@@ -270,525 +270,4 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
{{-- 礼包红包前端交互脚本已迁移到 resources/js/chat-room/red-packet-panel.js --}}
|
||||||
/**
|
|
||||||
* 礼包红包前端交互模块
|
|
||||||
*
|
|
||||||
* 功能:
|
|
||||||
* 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>
|
|
||||||
|
|||||||
Reference in New Issue
Block a user