openProposeModal() 改为 async: 1. 先调 /marriage/rings 检查背包 2. 无戒指 → 弹确认框 → 同意则新窗口打开 /shop 3. 有戒指 → 直接传入弹窗(openWithRings),避免二次请求 marriageProposeModal 新增 openWithRings(username, rings) 方法,接收预加载列表,无 loading 状态直接展示。
892 lines
42 KiB
PHP
892 lines
42 KiB
PHP
{{--
|
||
文件功能:婚姻系统前端弹窗组件集合
|
||
|
||
包含:
|
||
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, // 当前对方婚姻/求婚记录 ID(accept/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>
|