Files
chatroom/resources/js/chat-room/marriage-modals.js
T
2026-04-25 19:22:13 +08:00

921 lines
30 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 婚姻弹窗辅助入口,承接从 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 = `<span style="color:#9d174d;">${html}</span>`;
container.appendChild(div);
container.scrollTop = container.scrollHeight;
}
/**
* 打开求婚弹窗入口,从用户名片按钮触发。
*
* @param {string} username 求婚对象用户名
* @returns {Promise<void>}
*/
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<string, any>}
*/
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<Record<string, any>>} 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<string, any>}
*/
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<string, any>}
*/
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<string, any>}
*/
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<string, any>}
*/
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<string, any>}
*/
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<string, any>}
*/
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<string, any>}
*/
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<string, any>|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 `<button data-wedding-envelope-claim="${Number(ceremonyId)}"
style="display:inline-block; margin-left:10px; padding:4px 14px; border-radius:20px;
background:#d97706; color:#fff;
border:none; font-size:12px; font-weight:bold; cursor:pointer;
vertical-align:middle; line-height:1.8; box-shadow:0 2px 8px rgba(0,0,0,.3);"
title="点击领取婚礼红包">🧧 点击领取红包</button>`;
}
/**
* 将红包详情写入全局缓存,供领取按钮和状态面板复用。
*
* @param {number|string} ceremonyId 婚礼场次 ID
* @param {Record<string, any>} 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();
}