// 婚姻弹窗辅助入口,承接从 marriage-modals.blade.php 迁移出的全局函数。 /** * 向聊天主窗口追加一条婚姻系统公告,允许传入受控 HTML 按钮。 * * @param {string} html 系统模板生成的 HTML 内容 * @returns {void} */ export function appendSystemMessage(html) { const container = document.getElementById("chat-messages-container"); if (!container) { return; } const div = document.createElement("div"); div.style.cssText = "background:linear-gradient(135deg,#fdf4ff,#fce7f3); border-left:3px solid #ec4899; border-radius:6px; padding:5px 12px; margin:3px 0; font-size:13px; line-height:1.6;"; div.innerHTML = `${html}`; container.appendChild(div); container.scrollTop = container.scrollHeight; } /** * 打开求婚弹窗入口,从用户名片按钮触发。 * * @param {string} username 求婚对象用户名 * @returns {Promise} */ export async function openProposeModal(username) { let rings = []; try { const response = await fetch(window.chatContext.marriage.myRingsUrl, { headers: { Accept: "application/json", }, }); const data = await response.json(); if (data.status === "success") { rings = data.rings || []; } } catch { // 网络异常时继续交给后端兜底,避免前端误拦截真实可用的求婚流程。 } if (rings.length === 0) { const goShop = await window.chatDialog?.confirm( "求婚需要一枚💍结婚戒指,你的背包里还没有。\n\n要前往商店购买吗?", "需要结婚戒指", ); if (goShop && typeof window.openShopModal === "function") { window.openShopModal(); } return; } const modal = document.getElementById("marriage-propose-modal"); const alpine = window.Alpine; if (modal && alpine) { alpine.$data(modal).openWithRings(username, rings); } } /** * 打开婚礼设置弹窗,供新婚公告和名片入口复用。 * * @param {number|string} marriageId 婚姻记录 ID * @returns {void} */ export function openWeddingSetupModal(marriageId) { const modal = document.getElementById("wedding-setup-modal"); const alpine = window.Alpine; if (modal && alpine) { alpine.$data(modal).open(marriageId); } } /** * 创建求婚弹窗 Alpine 数据,负责戒指选择、婚礼档位和求婚提交。 * * @returns {Record} */ export function marriageProposeModal() { return { show: false, targetUsername: "", marriageId: null, rings: [], selectedRing: null, tiers: window.chatContext?.marriage?.weddingTiers || [], selectedTierId: window.chatContext?.marriage?.defaultWeddingTierId || "", loading: false, sending: false, error: "", get selectedTier() { if (!this.selectedTierId) { return null; } return this.tiers.find((tier) => tier.id == this.selectedTierId); }, get canAfford() { const amount = this.selectedTier ? Number(this.selectedTier.amount) : 0; return window.chatContext.userJjb >= amount; }, async open(username) { this.targetUsername = username; this.selectedRing = null; this.error = ""; this.loading = true; this.show = true; try { const response = await fetch(window.chatContext.marriage.myRingsUrl, { headers: { Accept: "application/json", }, }); const data = await response.json(); if (data.status === "success") { this.rings = data.rings; if (this.rings.length > 0) { this.selectedRing = this.rings[0].purchase_id; } } } catch { this.rings = []; } this.loading = false; }, /** * 使用预加载戒指列表打开弹窗,避免入口检查后重复请求背包。 * * @param {string} username 求婚对象用户名 * @param {Array>} rings 已加载的戒指列表 * @returns {void} */ openWithRings(username, rings) { this.targetUsername = username; this.error = ""; this.loading = false; this.rings = rings; this.selectedRing = rings.length > 0 ? rings[0].purchase_id : null; this.show = true; }, close() { this.show = false; }, async doPropose() { if (this.sending || !this.selectedRing) { return; } this.sending = true; this.error = ""; try { const response = await fetch(window.chatContext.marriage.proposeUrl, { method: "POST", headers: { "Content-Type": "application/json", Accept: "application/json", "X-CSRF-TOKEN": document.querySelector("meta[name=csrf-token]").content, }, body: JSON.stringify({ target_username: this.targetUsername, ring_purchase_id: this.selectedRing, wedding_tier_id: this.selectedTierId || null, room_id: window.chatContext.roomId, }), }); const data = await response.json(); if (data.status === "success") { this.close(); window.chatDialog?.alert("💍 求婚成功!等待对方回应(有效期 48 小时)", "已发出", "#f43f5e"); } else { this.error = data.message || "求婚失败"; } } catch { this.error = "网络异常,请稍后重试"; } this.sending = false; }, }; } /** * 创建收到求婚弹窗 Alpine 数据,处理同意和拒绝求婚。 * * @returns {Record} */ export function marriageIncomingModal() { return { show: false, proposerName: "", marriageId: null, ringName: "", ringIcon: "💍", expiresAt: "", acting: false, open(detail) { this.proposerName = detail.proposer_name || detail.proposer?.username || ""; this.marriageId = detail.marriage_id; this.ringName = detail.ring_name || ""; this.ringIcon = detail.ring_icon || "💍"; this.expiresAt = detail.expires_at || ""; this.show = true; }, close() { this.show = false; }, async doAccept() { if (this.acting) { return; } this.acting = true; try { const response = await fetch(window.chatContext.marriage.acceptUrl(this.marriageId), { method: "POST", headers: { "Content-Type": "application/json", Accept: "application/json", "X-CSRF-TOKEN": document.querySelector("meta[name=csrf-token]").content, }, body: JSON.stringify({ room_id: window.chatContext.roomId, }), }); const data = await response.json(); this.close(); if (data.status !== "success") { window.chatDialog?.alert(data.message || "操作失败", "提示", "#cc4444"); } } catch { window.chatDialog?.alert("网络异常", "错误", "#cc4444"); } this.acting = false; }, async doReject() { if (this.acting) { return; } this.acting = true; try { const response = await fetch(window.chatContext.marriage.rejectUrl(this.marriageId), { method: "POST", headers: { "Content-Type": "application/json", Accept: "application/json", "X-CSRF-TOKEN": document.querySelector("meta[name=csrf-token]").content, }, body: JSON.stringify({ room_id: window.chatContext.roomId, }), }); const data = await response.json(); this.close(); if (data.status !== "success") { window.chatDialog?.alert(data.message || "操作失败", "提示", "#cc4444"); } else { window.chatDialog?.alert("已婉拒对方的求婚", "操作完成", "#6b7280"); } } catch { window.chatDialog?.alert("网络异常", "错误", "#cc4444"); } this.acting = false; }, }; } /** * 创建结婚成功公告弹窗 Alpine 数据,并触发婚礼礼花特效。 * * @returns {Record} */ export function marriageAcceptedModal() { return { show: false, announcement: "", subText: "", marriageId: null, isNewlywed: false, open(detail) { this.marriageId = detail.marriage_id; const groomName = detail.user?.username ?? detail.groom_name ?? "??"; const brideName = detail.partner?.username ?? detail.bride_name ?? "??"; this.announcement = `${groomName} 与 ${brideName} 喜结连理!`; this.subText = detail.message || "愿百年好合,白头偕老!"; // 只有新婚双方本人可以继续打开婚礼设置弹窗。 const currentUsername = window.chatContext.username; this.isNewlywed = groomName === currentUsername || brideName === currentUsername; this.show = true; window.EffectManager?.play("wedding-fireworks"); }, close() { this.show = false; }, openWeddingSetup() { window.openWeddingSetupModal?.(this.marriageId); }, }; } /** * 创建离婚公告弹窗 Alpine 数据,并播放雷雨组合特效。 * * @returns {Record} */ export function marriageDivorcedModal() { return { show: false, announcement: "", subText: "", open(detail) { const userName = detail.user_username ?? detail.user?.username ?? "??"; const partnerName = detail.partner_username ?? detail.partner?.username ?? "??"; this.announcement = `${userName} 与 ${partnerName} 已解除婚姻关系`; this.subText = detail.message || "往昔已矣,各自珍重。"; this.show = true; window.EffectManager?.play("lightning"); // 雷电结束前后再叠加雨效,保留原本的离婚公告氛围。 setTimeout(() => { window.EffectManager?.play("rain"); }, 3500); }, close() { this.show = false; }, }; } /** * 创建发起离婚确认弹窗 Alpine 数据,展示协议/强制离婚的惩罚结果。 * * @returns {Record} */ export function divorceConfirmModal() { return { show: false, marriageId: null, mutualPenalty: 0, forcedPenalty: 0, mutualCooldown: 0, forcedCooldown: 0, acting: false, error: "", open(marriageId, config) { this.marriageId = marriageId; this.mutualPenalty = config.mutual_charm_penalty ?? 0; this.forcedPenalty = config.forced_charm_penalty ?? 0; this.mutualCooldown = config.mutual_cooldown_days ?? 0; this.forcedCooldown = config.forced_cooldown_days ?? 0; this.acting = false; this.error = ""; this.show = true; }, close() { this.show = false; }, async doConfirm() { if (this.acting) { return; } this.acting = true; this.error = ""; try { const response = await fetch(window.chatContext.marriage.divorceUrl(this.marriageId), { method: "POST", headers: { Accept: "application/json", "Content-Type": "application/json", "X-CSRF-TOKEN": document.querySelector("meta[name=csrf-token]").content, }, }); const data = await response.json(); this.close(); if (data.ok) { window.chatDialog?.alert(data.message, "申请已发出 📩", "#6b7280"); } else { window.chatDialog?.alert(data.message || "操作失败", "错误", "#dc2626"); } } catch { this.error = "网络请求失败,请重试。"; } finally { this.acting = false; } }, }; } /** * 创建离婚申请通知弹窗 Alpine 数据,处理同意或拒绝离婚请求。 * * @returns {Record} */ export function divorceRequestModal() { return { show: false, marriageId: null, initiatorName: "", mutualPenalty: 0, forcedPenalty: 0, acting: false, error: "", open(detail) { this.marriageId = detail.marriage_id; this.initiatorName = detail.initiator_name ?? detail.divorcer_username ?? "对方"; this.mutualPenalty = detail.mutual_charm_penalty ?? 0; this.forcedPenalty = detail.forced_charm_penalty ?? 0; this.acting = false; this.error = ""; this.show = true; }, close() { this.show = false; }, async doAgree() { if (this.acting) { return; } this.acting = true; this.error = ""; try { const response = await fetch(window.chatContext.marriage.confirmDivorceUrl(this.marriageId), { method: "POST", headers: { Accept: "application/json", "Content-Type": "application/json", "X-CSRF-TOKEN": document.querySelector("meta[name=csrf-token]").content, }, body: JSON.stringify({ room_id: window.chatContext.roomId, }), }); const data = await response.json(); this.close(); window.chatDialog?.alert(data.message, data.ok ? "操作完成" : "失败", data.ok ? "#6b7280" : "#cc4444"); } catch { this.error = "网络请求失败,请重试。"; } finally { this.acting = false; } }, async doReject() { if (this.acting) { return; } this.acting = true; this.error = ""; try { const response = await fetch(window.chatContext.marriage.rejectDivorceUrl(this.marriageId), { method: "POST", headers: { Accept: "application/json", "Content-Type": "application/json", "X-CSRF-TOKEN": document.querySelector("meta[name=csrf-token]").content, }, body: JSON.stringify({ room_id: window.chatContext.roomId, }), }); const data = await response.json(); this.close(); window.chatDialog?.alert(data.message, data.ok ? "已处理" : "失败", data.ok ? "#d97706" : "#cc4444"); } catch { this.error = "网络请求失败,请重试。"; } finally { this.acting = false; } }, }; } /** * 创建婚礼设置弹窗 Alpine 数据,负责选择婚礼档位并提交举办请求。 * * @returns {Record} */ export function weddingSetupModal() { return { show: false, marriageId: null, tiers: [], selectedTier: null, payBy: "groom", loading: false, sending: false, error: "", get myCost() { if (!this.selectedTier) { return 0; } return this.payBy === "split" ? Math.ceil(this.selectedTier.amount / 2) : this.selectedTier.amount; }, async open(marriageId) { this.marriageId = marriageId; this.selectedTier = null; this.payBy = "groom"; this.error = ""; this.loading = true; this.show = true; try { const response = await fetch(window.chatContext.marriage.weddingTiersUrl, { headers: { Accept: "application/json", }, }); const data = await response.json(); if (data.status === "success") { this.tiers = data.tiers; if (this.tiers.length > 0) { this.selectedTier = this.tiers[0]; } } } catch { this.tiers = []; } this.loading = false; }, close() { this.show = false; }, async doSetup() { if (this.sending || !this.selectedTier) { return; } this.error = ""; this.sending = true; try { // 当前产品只支持立即举办,前端固定 ceremony_type,避免保留无效时间选择状态。 const body = { tier_id: this.selectedTier.id, payer_type: this.payBy, ceremony_type: "immediate", room_id: window.chatContext.roomId, }; const response = await fetch(window.chatContext.marriage.weddingSetupUrl(this.marriageId), { method: "POST", headers: { "Content-Type": "application/json", Accept: "application/json", "X-CSRF-TOKEN": document.querySelector("meta[name=csrf-token]").content, }, body: JSON.stringify(body), }); const data = await response.json(); if (data.status === "success") { this.close(); window.chatDialog?.alert("🎊 婚礼已开始!红包正在分发给在线用户…", "设置成功", "#f59e0b"); } else { this.error = data.message || "设置失败"; } } catch { this.error = "网络异常,请稍后重试"; } this.sending = false; }, }; } /** * 创建婚礼红包弹窗 Alpine 数据,负责展示和领取婚礼红包。 * * @returns {Record} */ export function weddingEnvelopeModal() { return { show: false, marriageId: null, ceremonyId: null, title: "", subTitle: "", claimed: false, claiming: false, claimedAmount: 0, open(detail) { this.marriageId = detail.marriage_id; this.ceremonyId = detail.ceremony_id; const groomName = detail.groom_name ?? detail.user?.username ?? "??"; const brideName = detail.bride_name ?? detail.partner?.username ?? "??"; this.title = `${groomName} × ${brideName} 婚礼红包`; this.subTitle = detail.tier_name ? `【${detail.tier_name}】普天同庆` : "婚礼庆典红包"; this.claimed = false; this.claimedAmount = 0; this.show = true; }, close() { this.show = false; }, async doClaim() { if (this.claiming || this.claimed) { return; } this.claiming = true; try { const response = await fetch(`/wedding/ceremony/${this.ceremonyId}/claim`, { method: "POST", headers: { "Content-Type": "application/json", Accept: "application/json", "X-CSRF-TOKEN": document.querySelector("meta[name=csrf-token]").content, }, body: JSON.stringify({ ceremony_id: this.ceremonyId, }), }); const data = await response.json(); if (data.ok) { this.claimed = true; this.claimedAmount = data.amount || 0; } else { window.chatDialog?.alert(data.message || "领取失败", "提示", "#f59e0b"); // 已领取或已过期时同步锁定按钮,避免用户重复提交同一个红包。 if (data.message?.includes("已领取") || data.message?.includes("已过期")) { this.claimed = true; } } } catch { window.chatDialog?.alert("网络异常", "错误", "#cc4444"); } this.claiming = false; }, }; } /** * 读取 Alpine 组件数据,避免直接访问 Alpine 私有字段。 * * @param {string} modalId 弹窗节点 ID * @returns {Record|null} */ function getAlpineModalData(modalId) { const modal = document.getElementById(modalId); const alpine = window.Alpine; if (!modal || !alpine) { return null; } return alpine.$data(modal); } /** * 构建婚礼红包领取按钮 HTML,按钮通过 ceremonyId 读取全局缓存。 * * @param {number|string} ceremonyId 婚礼场次 ID * @returns {string} */ function buildWeddingClaimButton(ceremonyId) { return ``; } /** * 将红包详情写入全局缓存,供领取按钮和状态面板复用。 * * @param {number|string} ceremonyId 婚礼场次 ID * @param {Record} detail 红包详情 * @returns {void} */ function cacheWeddingEnvelope(ceremonyId, detail) { if (!window._weddingEnvelopes) { window._weddingEnvelopes = {}; } window._weddingEnvelopes[ceremonyId] = detail; } /** * 绑定婚姻系统广播事件,处理弹窗、公告和红包到账提示。 * * @returns {void} */ function bindMarriageModalEvents() { if (window.__marriageModalEventsBound) { return; } window.__marriageModalEventsBound = true; window.addEventListener("chat:marriage-accepted", (event) => { const detail = event.detail; const groomName = detail.user?.username ?? detail.groom_name ?? "??"; const brideName = detail.partner?.username ?? detail.bride_name ?? "??"; if (typeof window.appendSystemMessage === "function") { window.appendSystemMessage(`💑 ${groomName} 与 ${brideName} 喜结连理!`); } getAlpineModalData("marriage-accepted-modal")?.open(detail); }); window.addEventListener("chat:marriage-proposed", (event) => { getAlpineModalData("marriage-incoming-modal")?.open(event.detail); }); window.addEventListener("chat:wedding-celebration", (event) => { const detail = event.detail; const groomName = detail.user?.username ?? "??"; const brideName = detail.partner?.username ?? "??"; const tierIcon = detail.tier_icon ?? "🎊"; const tierName = detail.tier_name ?? "婚礼"; const amount = detail.total_amount ? Number(detail.total_amount).toLocaleString() : "?"; const ceremonyId = detail.ceremony_id; cacheWeddingEnvelope(ceremonyId, detail); if (typeof window.appendSystemMessage === "function") { window.appendSystemMessage( `${tierIcon} ${groomName} 与 ${brideName} 举办了【${tierName}】!总金额 💰${amount} 金币,快来抢红包!${buildWeddingClaimButton(ceremonyId)}`, ); } getAlpineModalData("wedding-envelope-modal")?.open(detail); }); window.addEventListener("chat:marriage-rejected", (event) => { const { partner_name: partnerName } = event.detail; window.chatDialog?.alert(`${partnerName} 婉拒了你的求婚,戒指随之遗失… 💔`, "求婚被拒绝", "#6b7280"); }); window.addEventListener("chat:marriage-expired", () => { window.chatDialog?.alert("你的求婚超时未获回应,戒指已消失… ⏰", "求婚已过期", "#9ca3af"); }); window.addEventListener("chat:divorce-requested", (event) => { getAlpineModalData("divorce-request-modal")?.open(event.detail); }); window.addEventListener("chat:envelope-claimed", (event) => { const { amount } = event.detail; window.chatDialog?.alert(`+${amount.toLocaleString()} 金币已到账 🎉`, "红包到手!", "#f59e0b"); }); window.addEventListener("chat:marriage-divorced", (event) => { const detail = event.detail; const userName = detail.user_username ?? detail.user?.username ?? "??"; const partnerName = detail.partner_username ?? detail.partner?.username ?? "??"; if (typeof window.appendSystemMessage === "function") { window.appendSystemMessage(`💔 ${userName} 与 ${partnerName} 解除了婚姻关系。`); } getAlpineModalData("marriage-divorced-modal")?.open(detail); }); } /** * 页面恢复时查询未领取婚礼红包,并补回公屏领取入口。 * * @returns {void} */ function restorePendingWeddingEnvelopes() { const deferBootstrap = window.deferChatGameBootstrap || ((callback, delay = 0) => setTimeout(callback, delay)); deferBootstrap(async () => { try { const response = await fetch("/wedding/pending-envelopes", { headers: { Accept: "application/json", }, }); const data = await response.json(); if (!response.ok || !data.envelopes?.length) { return; } data.envelopes.forEach((envelope) => { const ceremonyId = envelope.ceremony_id; cacheWeddingEnvelope(ceremonyId, { ceremony_id: ceremonyId, total_amount: envelope.total_amount, tier_name: envelope.tier_name, tier_icon: envelope.tier_icon, user: { username: envelope.groom, }, partner: { username: envelope.bride, }, }); if (typeof window.appendSystemMessage === "function") { window.appendSystemMessage( `⚠️ 您有来自 ${envelope.tier_icon} ${envelope.groom} 与 ${envelope.bride}【${envelope.tier_name}】的婚礼红包未领取!${buildWeddingClaimButton(ceremonyId)}`, ); } }); } catch (error) { console.warn("[婚礼红包] 恢复待领取按钮失败", error); } }, 3000); } /** * 页面就绪后初始化婚姻私人频道和待领取红包恢复。 * * @returns {void} */ function bindMarriageModalBootstrap() { document.addEventListener("DOMContentLoaded", () => { const userId = window.chatContext?.userId; if (userId && typeof window.initMarriagePrivateChannel === "function") { // Echo 初始化可能晚于 DOM 就绪,保留短延迟避免私人频道注册失败。 setTimeout(() => window.initMarriagePrivateChannel(userId), 1500); } restorePendingWeddingEnvelopes(); }); } /** * 暴露婚姻弹窗存量入口,兼容 Blade 内 onclick、Alpine 方法和广播回调。 * * @returns {void} */ export function bindMarriageModalControls() { window.appendSystemMessage = appendSystemMessage; window.divorceConfirmModal = divorceConfirmModal; window.divorceRequestModal = divorceRequestModal; window.marriageAcceptedModal = marriageAcceptedModal; window.marriageDivorcedModal = marriageDivorcedModal; window.marriageIncomingModal = marriageIncomingModal; window.marriageProposeModal = marriageProposeModal; window.openProposeModal = openProposeModal; window.openWeddingSetupModal = openWeddingSetupModal; window.weddingEnvelopeModal = weddingEnvelopeModal; window.weddingSetupModal = weddingSetupModal; bindMarriageModalEvents(); bindMarriageModalBootstrap(); }