Files
chatroom/resources/views/chat/partials/marriage-modals.blade.php
lkddi 1f33013216 优化:求婚前先检查戒指库存,无戒指则引导购买
openProposeModal() 改为 async:
1. 先调 /marriage/rings 检查背包
2. 无戒指 → 弹确认框 → 同意则新窗口打开 /shop
3. 有戒指 → 直接传入弹窗(openWithRings),避免二次请求

marriageProposeModal 新增 openWithRings(username, rings)
方法,接收预加载列表,无 loading 状态直接展示。
2026-03-01 15:38:52 +08:00

892 lines
42 KiB
PHP
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.
{{--
文件功能:婚姻系统前端弹窗组件集合
包含:
1. 求婚弹窗(#marriage-propose-modal选戒指→求婚
2. 求婚接收弹窗(#marriage-incoming-modal被求婚方同意/拒绝
3. 结婚成功弹窗(#marriage-accepted-modal恭喜UI + 婚礼设置入口
4. 婚礼设置弹窗(#wedding-setup-modal选档位→立即/定时→支付
5. 婚礼红包弹窗(#wedding-envelope-modal全局弹出点击领取
6. 婚姻状态弹窗(#marriage-status-modal查看自己的婚姻信息
7. 名片婚姻信息区(注入到 user-card JS 组件中)
@author ChatRoom Laravel
@version 1.0.0
--}}
{{-- ═══════════ 1. 求婚弹窗 ═══════════ --}}
<div id="marriage-propose-modal" x-data="marriageProposeModal()" style="display:none">
<div x-show="show"
style="display:none; position:fixed; inset:0; background:rgba(0,0,0,.6);
z-index:9800; display:flex; align-items:center; justify-content:center;"
x-on:click.self="close()">
<div
style="width:420px; max-width:95vw; background:#fff; border-radius:20px;
box-shadow:0 20px 60px rgba(219,39,119,.3); overflow:hidden;">
{{-- 头部 --}}
<div
style="background:linear-gradient(135deg,#f43f5e,#ec4899); padding:16px 20px;
display:flex; align-items:center; justify-content:space-between;">
<div>
<div style="color:#fff; font-weight:bold; font-size:17px;">💍 TA 求婚</div>
<div style="color:rgba(255,255,255,.9); font-size:12px; margin-top:2px;"
x-text="'对象:' + targetUsername"></div>
</div>
<button x-on:click="close()"
style="background:rgba(255,255,255,.25); border:none; color:#fff; width:30px;
height:30px; border-radius:50%; cursor:pointer; font-size:18px;">×</button>
</div>
<div style="padding:20px;">
{{-- 错误提示 --}}
<div x-show="error"
style="display:none; background:#fef2f2; border:1px solid #fecaca;
color:#dc2626; border-radius:8px; padding:10px 14px; font-size:12px; margin-bottom:14px;"
x-text="error"></div>
{{-- 加载中 --}}
<div x-show="loading" style="text-align:center; padding:20px; color:#ec4899;">
加载戒指列表…
</div>
{{-- 戒指选择 --}}
<div x-show="!loading">
<div style="font-size:13px; font-weight:bold; color:#374151; margin-bottom:10px;">
选择求婚戒指 <span
style="font-weight:normal; color:#9ca3af; font-size:11px;">(戒指将被使用消耗。若对方拒绝,戒指遗失)</span>
</div>
<div x-show="rings.length === 0"
style="text-align:center; padding:20px; color:#9ca3af; font-size:13px;">
你没有可用的戒指 😢<br>
<a href="/shop"
style="color:#ec4899; font-size:12px; margin-top:6px; display:inline-block;">前往商店购买戒指 </a>
</div>
<div style="display:grid; grid-template-columns:repeat(2,1fr); gap:10px; margin-bottom:16px;">
<template x-for="ring in rings" :key="ring.purchase_id">
<div x-on:click="selectedRing = ring.purchase_id"
:style="selectedRing === ring.purchase_id ?
'border:2px solid #f43f5e; background:#fff1f2;' :
'border:2px solid #e2e8f0; background:#fff;'"
style="padding:12px; border-radius:12px; cursor:pointer; transition:all .15s; text-align:center;">
<div style="font-size:26px; margin-bottom:4px;" x-text="ring.icon"></div>
<div style="font-weight:bold; font-size:12px; color:#374151;" x-text="ring.name"></div>
<div style="font-size:11px; color:#f43f5e; margin-top:2px;"
x-text="'💞 初始亲密度 +' + ring.intimacy_bonus"></div>
<div style="font-size:10px; color:#9ca3af;"
x-text="'魅力 +' + ring.charm_bonus + ' (结婚时)'"></div>
</div>
</template>
</div>
<button x-on:click="doPropose()" :disabled="sending || !selectedRing || rings.length === 0"
style="width:100%; padding:12px; border:none; border-radius:12px; font-size:14px;
font-weight:bold; cursor:pointer; transition:all .2s;"
:style="(sending || !selectedRing || rings.length === 0) ?
'background:#f1f5f9; color:#94a3b8; cursor:not-allowed;' :
'background:linear-gradient(135deg,#f43f5e,#ec4899); color:#fff;'">
<span x-text="sending ? '正在求婚…' : '💍 确认求婚'"></span>
</button>
</div>
</div>
</div>
</div>
</div>
{{-- ═══════════ 2. 收到求婚弹窗 ═══════════ --}}
<div id="marriage-incoming-modal" x-data="marriageIncomingModal()" style="display:none">
<div x-show="show"
style="display:none; position:fixed; inset:0; background:rgba(0,0,0,.65);
z-index:9850; display:flex; align-items:center; justify-content:center;">
<div
style="width:380px; max-width:95vw; background:#fff; border-radius:20px;
box-shadow:0 20px 60px rgba(244,63,94,.4); overflow:hidden; text-align:center;">
<div style="background:linear-gradient(135deg,#f43f5e,#f97316); padding:20px;">
<div style="font-size:40px; margin-bottom:6px;">💌</div>
<div style="color:#fff; font-weight:bold; font-size:18px;">收到求婚啦!</div>
</div>
<div style="padding:20px;">
<div style="font-size:14px; color:#374151; margin-bottom:6px;">
<strong x-text="proposerName" style="color:#f43f5e;"></strong>
向你送上了求婚戒指
</div>
<div x-show="ringName"
style="display:inline-flex; align-items:center; gap:6px;
background:#fff1f2; border:1px solid #fecdd3; border-radius:30px;
padding:6px 14px; font-size:13px; color:#e11d48; margin-bottom:4px;">
<span x-text="ringIcon" style="font-size:18px;"></span>
<span x-text="ringName"></span>
</div>
<div style="font-size:11px; color:#9ca3af; margin-bottom:18px;">
戒指求婚有效期 <strong>48小时</strong>,过期自动失效且戒指遗失
</div>
<div x-show="expiresAt" style="font-size:11px; color:#f59e0b; margin-bottom:14px;"
x-text="'过期时间:' + expiresAt"></div>
<div style="display:flex; gap:10px;">
<button x-on:click="doReject()" :disabled="acting"
style="flex:1; padding:11px; border-radius:10px; font-size:13px; font-weight:bold;
cursor:pointer; background:#f1f5f9; color:#374151; border:1px solid #e2e8f0;">
<span x-text="acting ? '处理中…' : '😢 婉拒'"></span>
</button>
<button x-on:click="doAccept()" :disabled="acting"
style="flex:2; padding:11px; border-radius:10px; font-size:13px; font-weight:bold;
cursor:pointer; background:linear-gradient(135deg,#f43f5e,#ec4899);
color:#fff; border:none;">
<span x-text="acting ? '处理中…' : '💑 答应啦!'"></span>
</button>
</div>
</div>
</div>
</div>
</div>
{{-- ═══════════ 3. 结婚成功全屏公告 ═══════════ --}}
<div id="marriage-accepted-modal" x-data="marriageAcceptedModal()" style="display:none">
<div x-show="show" x-transition
style="display:none; position:fixed; inset:0; background:rgba(0,0,0,.7);
z-index:9900; display:flex; align-items:center; justify-content:center;">
<div
style="width:460px; max-width:95vw; background:#fff; border-radius:24px;
box-shadow:0 24px 80px rgba(244,63,94,.5); overflow:hidden; text-align:center;">
<div style="background:linear-gradient(135deg,#f43f5e,#ec4899,#a855f7); padding:28px 20px;">
<div style="font-size:50px; margin-bottom:8px;">💑</div>
<div style="color:#fff; font-weight:bold; font-size:20px; margin-bottom:4px;">
🎊 天作之合!
</div>
<div style="color:rgba(255,255,255,.9); font-size:15px;" x-text="announcement"></div>
</div>
<div style="padding:20px 24px;">
<div style="font-size:13px; color:#6b7280; margin-bottom:16px;" x-text="subText"></div>
{{-- 举办婚礼按钮(仅对新婚夫妻显示) --}}
<div x-show="isNewlywed"
style="display:none; display:flex; gap:10px; justify-content:center; margin-bottom:14px;">
<button x-on:click="openWeddingSetup(); close();"
style="padding:11px 24px; background:linear-gradient(135deg,#f59e0b,#d97706);
color:#fff; border:none; border-radius:12px; font-size:13px; font-weight:bold; cursor:pointer;">
🎊 立即举办婚礼!
</button>
</div>
<button x-on:click="close()"
style="padding:10px 32px; background:#f1f5f9; border:none; border-radius:10px;
font-size:13px; color:#6b7280; cursor:pointer;">关闭</button>
</div>
</div>
</div>
</div>
{{-- ═══════════ 4. 婚礼设置弹窗 ═══════════ --}}
<div id="wedding-setup-modal" x-data="weddingSetupModal()" style="display:none">
<div x-show="show"
style="display:none; position:fixed; inset:0; background:rgba(0,0,0,.65);
z-index:9820; display:flex; align-items:center; justify-content:center;">
<div
style="width:500px; max-width:95vw; background:#fff; border-radius:20px;
box-shadow:0 20px 60px rgba(245,158,11,.3); overflow:hidden;">
<div
style="background:linear-gradient(135deg,#f59e0b,#d97706); padding:16px 20px;
display:flex; align-items:center; justify-content:space-between;">
<div>
<div style="color:#fff; font-weight:bold; font-size:17px;">🎊 举办婚礼庆典</div>
<div style="color:rgba(255,255,255,.85); font-size:12px; margin-top:2px;">宴请全场,赠送红包大礼</div>
</div>
<button x-on:click="close()"
style="background:rgba(255,255,255,.25); border:none; color:#fff;
width:30px; height:30px; border-radius:50%; cursor:pointer; font-size:18px;">×</button>
</div>
<div style="padding:20px;">
<div x-show="error"
style="display:none; background:#fef2f2; border:1px solid #fecaca;
color:#dc2626; border-radius:8px; padding:10px 14px; font-size:12px; margin-bottom:14px;"
x-text="error"></div>
{{-- 档位选择 --}}
<div style="font-size:13px; font-weight:bold; color:#374151; margin-bottom:10px;">选择庆典档位</div>
<div x-show="loading" style="text-align:center; padding:16px; color:#f59e0b;">加载档位中…</div>
<div x-show="!loading"
style="display:grid; grid-template-columns:repeat(5,1fr); gap:8px; margin-bottom:16px;">
<template x-for="tier in tiers" :key="tier.id">
<div x-on:click="selectedTier = tier"
:style="selectedTier && selectedTier.id === tier.id ?
'border:2px solid #f59e0b; background:#fffbeb;' :
'border:2px solid #e2e8f0; background:#fff;'"
style="padding:10px 6px; border-radius:10px; cursor:pointer; text-align:center; transition:all .15s;">
<div style="font-size:22px;" x-text="tier.icon"></div>
<div style="font-size:11px; font-weight:bold; color:#374151; margin-top:3px;"
x-text="tier.name"></div>
<div style="font-size:11px; color:#f59e0b; font-weight:bold;"
x-text="tier.amount.toLocaleString() + ' 金'"></div>
</div>
</template>
</div>
{{-- 支付方式 --}}
<div x-show="selectedTier" style="display:none;">
<div style="font-size:13px; font-weight:bold; color:#374151; margin-bottom:10px;">金币支付方式</div>
<div style="display:flex; gap:10px; margin-bottom:16px;">
<label
style="flex:1; display:flex; align-items:center; gap:8px; padding:10px;
border-radius:10px; cursor:pointer;"
:style="payBy === 'groom' ? 'border:2px solid #f59e0b; background:#fffbeb;' :
'border:2px solid #e2e8f0;'">
<input type="radio" x-model="payBy" value="groom" style="accent-color:#f59e0b;">
<span style="font-size:12px; font-weight:bold;">由我全额支付</span>
</label>
<label
style="flex:1; display:flex; align-items:center; gap:8px; padding:10px;
border-radius:10px; cursor:pointer;"
:style="payBy === 'split' ? 'border:2px solid #f59e0b; background:#fffbeb;' :
'border:2px solid #e2e8f0;'">
<input type="radio" x-model="payBy" value="split" style="accent-color:#f59e0b;">
<span style="font-size:12px; font-weight:bold;">双方各出一半</span>
</label>
</div>
{{-- 时间选择 --}}
<div style="font-size:13px; font-weight:bold; color:#374151; margin-bottom:10px;">举办时间</div>
<div style="display:flex; gap:10px; margin-bottom:16px;">
<label
style="flex:1; display:flex; align-items:center; gap:8px; padding:10px;
border-radius:10px; cursor:pointer;"
:style="timing === 'now' ? 'border:2px solid #ec4899; background:#fff1f2;' :
'border:2px solid #e2e8f0;'">
<input type="radio" x-model="timing" value="now" style="accent-color:#ec4899;">
<div>
<div style="font-size:12px; font-weight:bold;">🎊 立即举办</div>
<div style="font-size:10px; color:#9ca3af;">随机红包即刻发出</div>
</div>
</label>
<label
style="flex:1; display:flex; align-items:center; gap:8px; padding:10px;
border-radius:10px; cursor:pointer;"
:style="timing === 'scheduled' ? 'border:2px solid #ec4899; background:#fff1f2;' :
'border:2px solid #e2e8f0;'">
<input type="radio" x-model="timing" value="scheduled" style="accent-color:#ec4899;">
<div>
<div style="font-size:12px; font-weight:bold;"> 定时举办</div>
<div style="font-size:10px; color:#9ca3af;">选一个吉时</div>
</div>
</label>
</div>
<div x-show="timing === 'scheduled'" style="display:none; margin-bottom:16px;">
<input type="datetime-local" x-model="scheduledAt"
style="width:100%; padding:10px; border:1px solid #fcd34d; border-radius:8px;
font-size:13px; box-sizing:border-box;">
</div>
{{-- 费用摘要 --}}
<div x-show="selectedTier"
style="display:none; background:#fffbeb; border:1px solid #fde68a;
border-radius:10px; padding:12px 14px; font-size:12px; color:#92400e; margin-bottom:16px;">
<div style="display:flex; justify-content:space-between; margin-bottom:4px;">
<span>红包总额</span>
<strong x-text="selectedTier ? selectedTier.amount.toLocaleString() + ' 金' : '—'"></strong>
</div>
<div style="display:flex; justify-content:space-between; margin-bottom:4px;">
<span>你需支付</span>
<strong x-text="myCost + ' 金'"></strong>
</div>
<div x-show="payBy === 'split'" style="display:none; font-size:11px; color:#b45309;">
另一半将在婚礼触发后自动从另一方账号扣除
</div>
</div>
<button x-on:click="doSetup()" :disabled="sending || !selectedTier"
style="width:100%; padding:13px; border:none; border-radius:12px; font-size:14px;
font-weight:bold; cursor:pointer; transition:all .2s;"
:style="(sending || !selectedTier) ?
'background:#f1f5f9; color:#94a3b8; cursor:not-allowed;' :
'background:linear-gradient(135deg,#f59e0b,#d97706); color:#fff;'">
<span x-text="sending ? '设置中…' : '🎊 确认举办婚礼'"></span>
</button>
</div>
</div>
</div>
</div>
</div>
{{-- ═══════════ 5. 婚礼红包弹窗(全局触发) ═══════════ --}}
<div id="wedding-envelope-modal" x-data="weddingEnvelopeModal()" style="display:none">
<div x-show="show" x-transition
style="display:none; position:fixed; inset:0; background:rgba(0,0,0,.6);
z-index:9910; display:flex; align-items:center; justify-content:center;">
<div
style="width:380px; max-width:95vw; background:linear-gradient(160deg,#7c2d12,#9a3412);
border-radius:20px; box-shadow:0 24px 80px rgba(220,38,38,.5); overflow:hidden; text-align:center;
border:1px solid rgba(251,146,60,.3);">
<div style="padding:24px 20px 16px;">
<div style="font-size:52px; margin-bottom:8px; animation:pulse 1.5s infinite;">🧧</div>
<div style="color:#fef3c7; font-weight:bold; font-size:18px; margin-bottom:4px;" x-text="title"></div>
<div style="color:rgba(254,243,199,.7); font-size:12px; margin-bottom:16px;" x-text="subTitle"></div>
{{-- 未领取 --}}
<div x-show="!claimed" style="display:none;">
<div style="color:rgba(254,243,199,.5); font-size:11px; margin-bottom:12px;">
红包有效期 <strong style="color:#fcd34d;">24小时</strong>,过期自动消失
</div>
<button x-on:click="doClaim()" :disabled="claiming"
style="padding:14px 40px; border:none; border-radius:50px; font-size:16px;
font-weight:bold; cursor:pointer; transition:all .2s;"
:style="claiming
?
'background:#fcd34d; color:#92400e; opacity:.7; cursor:not-allowed;' :
'background:linear-gradient(135deg,#fcd34d,#f59e0b); color:#92400e;'">
<span x-text="claiming ? '领取中…' : '🎁 点击领取'"></span>
</button>
</div>
{{-- 已领取 --}}
<div x-show="claimed" style="display:none;">
<div style="font-size:36px; margin-bottom:6px; color:#fcd34d; font-weight:bold;"
x-text="'+' + claimedAmount.toLocaleString() + ' 金'"></div>
<div style="color:#fef3c7; font-size:13px;">🎉 恭喜你领取了红包!</div>
<div style="color:rgba(254,243,199,.6); font-size:11px; margin-top:4px;">金币已自动到账</div>
</div>
</div>
<div style="padding:0 20px 20px;">
<button x-on:click="close()"
style="padding:10px 32px; background:rgba(0,0,0,.3); border:none; border-radius:30px;
font-size:12px; color:rgba(254,243,199,.7); cursor:pointer;">
<span x-text="claimed ? '收下啦 ✨' : '关闭'"></span>
</button>
</div>
</div>
</div>
</div>
<style>
@keyframes pulse {
0%,
100% {
transform: scale(1);
}
50% {
transform: scale(1.08);
}
}
</style>
{{-- ═══════════ Alpine.js 组件脚本 ═══════════ --}}
<script>
/**
* 求婚弹窗组件
*/
function marriageProposeModal() {
return {
show: false,
targetUsername: '',
marriageId: null, // 当前对方婚姻/求婚记录 IDaccept/reject 用)
rings: [],
selectedRing: null,
loading: false,
sending: false,
error: '',
async open(username) {
this.targetUsername = username;
this.selectedRing = null;
this.error = '';
this.loading = true;
this.show = true;
try {
const res = await fetch(window.chatContext.marriage.myRingsUrl, {
headers: {
'Accept': 'application/json'
}
});
const data = await res.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;
},
/**
* 由 openProposeModal() 传入已预加载的戒指列表,无需二次请求。
* @param {string} username 求婚对象用户名
* @param {Array} rings 已加载的戒指列表
*/
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 res = 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,
room_id: window.chatContext.roomId
})
});
const data = await res.json();
if (data.status === 'success') {
this.close();
window.chatDialog?.alert('💍 求婚成功!等待对方回应(有效期 48 小时)', '已发出', '#f43f5e');
} else {
this.error = data.message || '求婚失败';
}
} catch {
this.error = '网络异常,请稍后重试';
}
this.sending = false;
}
};
}
/**
* 收到求婚弹窗组件
*/
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 res = 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 res.json();
this.close();
if (data.status !== 'success') {
window.chatDialog?.alert(data.message || '操作失败', '提示', '#cc4444');
}
// 结婚成功后会收到 MarriageAccepted 全局广播
} catch {
window.chatDialog?.alert('网络异常', '错误', '#cc4444');
}
this.acting = false;
},
async doReject() {
if (this.acting) return;
this.acting = true;
try {
const res = 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 res.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;
}
};
}
/**
* 结婚成功弹窗组件(收到全局广播后触发)
*/
function marriageAcceptedModal() {
return {
show: false,
announcement: '',
subText: '',
marriageId: null,
isNewlywed: false,
open(detail) {
this.marriageId = detail.marriage_id;
this.announcement = `${detail.groom_name} 与 ${detail.bride_name} 喜结连理!`;
this.subText = detail.message || '愿百年好合,白头偕老!';
// 仅当前用户是新婚双方之一时显示举办婚礼按钮
const me = window.chatContext.username;
this.isNewlywed = (detail.groom_name === me || detail.bride_name === me);
this.show = true;
// 播放烟花特效
if (window.EffectManager) {
window.EffectManager.play('fireworks');
}
},
close() {
this.show = false;
},
openWeddingSetup() {
openWeddingSetupModal(this.marriageId);
}
};
}
/**
* 婚礼设置弹窗组件
*/
function weddingSetupModal() {
return {
show: false,
marriageId: null,
tiers: [],
selectedTier: null,
payBy: 'groom',
timing: 'now',
scheduledAt: '',
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.timing = 'now';
this.scheduledAt = '';
this.error = '';
this.loading = true;
this.show = true;
try {
const res = await fetch(window.chatContext.marriage.weddingTiersUrl, {
headers: {
'Accept': 'application/json'
}
});
const data = await res.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 {
const body = {
tier_id: this.selectedTier.id,
pay_by: this.payBy,
ceremony_type: this.timing,
room_id: window.chatContext.roomId,
};
if (this.timing === 'scheduled') {
body.scheduled_at = this.scheduledAt;
}
const res = 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 res.json();
if (data.status === 'success') {
this.close();
window.chatDialog?.alert(
this.timing === 'now' ?
'🎊 婚礼已开始!红包正在分发给在线用户…' :
'⏰ 婚礼已预约,时间到时将自动举办!',
'设置成功', '#f59e0b'
);
} else {
this.error = data.message || '设置失败';
}
} catch {
this.error = '网络异常,请稍后重试';
}
this.sending = false;
}
};
}
/**
* 婚礼红包弹窗组件
*/
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;
this.title = `${detail.groom_name} × ${detail.bride_name} 婚礼红包`;
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 res = await fetch(`/wedding/${this.marriageId}/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 res.json();
if (data.status === 'success') {
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;
}
};
}
// ───────── 全局入口函数 ─────────────────────────────────
/**
* 打开求婚弹窗(从名片按钮调用)。
* 先检查背包是否有戒指:
* - 有 → 直接开弹窗(戒指列表已预加载,无需二次请求)
* - 无 → 提示用户前往商店购买
*/
async function openProposeModal(username) {
// 显示加载中(通过按钮禁用已阻止,这里只做静默检查)
let rings = [];
try {
const res = await fetch(window.chatContext.marriage.myRingsUrl, {
headers: {
'Accept': 'application/json'
}
});
const data = await res.json();
if (data.status === 'success') {
rings = data.rings || [];
}
} catch {
/* 网络异常时继续走有戒指逻辑(后端再兜底) */
}
if (rings.length === 0) {
// 没有戒指:弹确认框引导购买
const goShop = await window.chatDialog?.confirm(
'求婚需要一枚💍结婚戒指,你的背包里还没有。\n\n要前往商店购买吗',
'需要结婚戒指'
);
if (goShop) {
window.open('/shop', '_blank');
}
return;
}
// 有戒指:打开弹窗,将已加载的列表传入(避免二次请求)
const el = document.getElementById('marriage-propose-modal');
if (el) Alpine.$data(el).openWithRings(username, rings);
}
/** 打开婚礼设置弹窗 */
function openWeddingSetupModal(marriageId) {
const el = document.getElementById('wedding-setup-modal');
if (el) Alpine.$data(el).open(marriageId);
}
// ───────── WebSocket 事件处理 ───────────────────────────
/** 收到全局结婚公告 */
window.addEventListener('chat:marriage-accepted', (e) => {
const detail = e.detail;
const el = document.getElementById('marriage-accepted-modal');
if (el) Alpine.$data(el).open(detail);
});
/** 收到求婚通知(私人频道,目标方) */
window.addEventListener('chat:marriage-proposed', (e) => {
const detail = e.detail;
const el = document.getElementById('marriage-incoming-modal');
if (el) Alpine.$data(el).open(detail);
});
/** 收到婚礼庆典(全局,显示红包) */
window.addEventListener('chat:wedding-celebration', (e) => {
const detail = e.detail;
const el = document.getElementById('wedding-envelope-modal');
if (el) Alpine.$data(el).open(detail);
});
/** 求婚被拒(私人频道,发起方) */
window.addEventListener('chat:marriage-rejected', (e) => {
const {
proposer_name,
partner_name
} = e.detail;
window.chatDialog?.alert(
`${partner_name} 婉拒了你的求婚,戒指随之遗失… 💔`,
'求婚被拒绝',
'#6b7280'
);
});
/** 求婚超时(私人频道,发起方) */
window.addEventListener('chat:marriage-expired', (e) => {
window.chatDialog?.alert(
'你的求婚超时未获回应,戒指已消失… ⏰',
'求婚已过期',
'#9ca3af'
);
});
/** 接到协议离婚申请(私人频道,对方) */
window.addEventListener('chat:divorce-requested', (e) => {
const {
initiator_name,
marriage_id
} = e.detail;
window.chatDialog?.confirm(
`${initiator_name} 申请与你协议离婚,是否同意?`,
'离婚申请 💔'
).then(ok => {
if (!ok) return;
fetch(window.chatContext.marriage.confirmDivorceUrl(marriage_id), {
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
})
}).then(r => r.json()).then(data => {
window.chatDialog?.alert(data.message, data.status === 'success' ? '操作完成' :
'失败', data.status === 'success' ? '#6b7280' : '#cc4444');
});
});
});
/** 红包领取成功通知(私人频道) */
window.addEventListener('chat:envelope-claimed', (e) => {
const {
amount
} = e.detail;
window.chatDialog?.alert(`+${amount.toLocaleString()} 金币已到账 🎉`, '红包到手!', '#f59e0b');
});
/** 结婚/离婚全局公告:在聊天消息区追加一条系统消息 */
window.addEventListener('chat:marriage-accepted', (e) => {
const {
groom_name,
bride_name,
message
} = e.detail;
if (typeof appendSystemMessage === 'function') {
appendSystemMessage(`💑 ${groom_name} 与 ${bride_name} 喜结连理!${message || ''}`);
}
});
window.addEventListener('chat:marriage-divorced', (e) => {
const {
user_name,
partner_name
} = e.detail;
if (typeof appendSystemMessage === 'function') {
appendSystemMessage(`💔 ${user_name} 与 ${partner_name} 解除了婚姻关系。`);
}
});
/** 页面加载完成后初始化私人频道监听 */
document.addEventListener('DOMContentLoaded', () => {
const userId = window.chatContext?.userId;
if (userId && typeof window.initMarriagePrivateChannel === 'function') {
// 延迟初始化,确保 Echo 已就绪
setTimeout(() => window.initMarriagePrivateChannel(userId), 1500);
}
});
</script>