Files
chatroom/resources/views/chat/partials/marriage-modals.blade.php

1005 lines
48 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()" x-show="show" x-cloak>
<div style="position:fixed; inset:0; background:rgba(15,5,25,.75); backdrop-filter:blur(4px);
z-index:9800; display:flex; align-items:center; justify-content:center; padding:16px;"
x-on:click.self="close()">
<div x-transition:enter="transition ease-out duration-200" x-transition:enter-start="opacity-0 scale-95"
x-transition:enter-end="opacity-100 scale-100"
style="width:420px; max-width:100%; border-radius:24px; overflow:hidden;
box-shadow:0 32px 80px rgba(244,63,94,.45), 0 0 0 1px rgba(244,63,94,.15);">
{{-- ───── 封面区域 ────── --}}
<div
style="background:linear-gradient(145deg,#be185d 0%,#f43f5e 45%,#fb7185 80%,#fda4af 100%);
padding:32px 24px 24px; text-align:center; position:relative;">
{{-- 关闭按钮 --}}
<button x-on:click="close()"
style="position:absolute; top:14px; right:14px; background:rgba(255,255,255,.2);
border:none; color:#fff; width:32px; height:32px; border-radius:50%;
cursor:pointer; font-size:16px; line-height:32px; transition:background .15s;"
onmouseover="this.style.background='rgba(255,255,255,.35)'"
onmouseout="this.style.background='rgba(255,255,255,.2)'"></button>
{{-- 大戒指图标 --}}
<div style="font-size:52px; margin-bottom:12px; filter:drop-shadow(0 4px 12px rgba(0,0,0,.25));">💍
</div>
{{-- 标题 --}}
<div style="color:#fff; font-size:22px; font-weight:800; letter-spacing:.5px; margin-bottom:6px;">
TA 求婚
</div>
{{-- 对象名 --}}
<div
style="display:inline-flex; align-items:center; gap:6px;
background:rgba(255,255,255,.18); border-radius:999px;
padding:4px 14px; font-size:13px; color:#fff;">
<span>💕</span>
<span x-text="targetUsername" style="font-weight:bold;"></span>
</div>
</div>
{{-- ───── 内容区域 ────── --}}
<div style="background:#fff; padding:24px;">
{{-- 错误提示 --}}
<div x-show="error" x-transition
style="display:none; background:#fef2f2; border-left:3px solid #f43f5e;
color:#dc2626; border-radius:8px; padding:10px 14px; font-size:12px; margin-bottom:16px;"
x-text="error"></div>
{{-- 加载中 --}}
<div x-show="loading" style="text-align:center; padding:28px; color:#f43f5e;">
<div style="font-size:28px; margin-bottom:8px;">💍</div>
<div style="font-size:13px; color:#9ca3af;">正在加载戒指列表…</div>
</div>
{{-- 戒指展示区 --}}
<div x-show="!loading">
{{-- 区域标题 --}}
<div style="display:flex; align-items:center; gap:8px; margin-bottom:14px;">
<div
style="width:3px; height:16px; background:linear-gradient(#f43f5e,#ec4899); border-radius:2px;">
</div>
<span style="font-size:13px; font-weight:700; color:#1f2937;">赠送的求婚戒</span>
<span style="font-size:11px; color:#d1d5db;">戒指消耗后不退,拒绝则遗失</span>
</div>
{{-- 无戒指提示 --}}
<div x-show="rings.length === 0"
style="text-align:center; padding:28px 16px; background:#fdf2f8; border-radius:14px;
border:1.5px dashed #fbcfe8; margin-bottom:16px;">
<div style="font-size:32px; margin-bottom:8px;">💔</div>
<div style="font-size:13px; color:#9ca3af; margin-bottom:10px;">背包里还没有戒指哦</div>
<button onclick="if(typeof window.openShopModal==='function') window.openShopModal()"
style="background:linear-gradient(135deg,#f43f5e,#ec4899); color:#fff; border:none;
border-radius:8px; padding:7px 18px; font-size:12px; font-weight:bold; cursor:pointer;">
🛒 前往商店购买
</button>
</div>
{{-- 戒指展示(居中,只展示第一枚,不可选) --}}
<div x-show="rings.length > 0" style="display:flex; justify-content:center; margin-bottom:18px;">
<template x-if="rings.length > 0">
<div
style="text-align:center; padding:18px 28px; border-radius:16px;
background:linear-gradient(135deg,#fff1f2,#fdf2f8);
border:2px solid #f43f5e; box-shadow:0 4px 18px rgba(244,63,94,.15);
min-width:140px;">
<div style="font-size:42px; margin-bottom:8px;" x-text="rings[0].icon"></div>
<div style="font-weight:700; font-size:14px; color:#1f2937; margin-bottom:6px;"
x-text="rings[0].name"></div>
<div style="font-size:11px; color:#f43f5e; font-weight:600;"
x-text="'💞 亲密 +' + rings[0].intimacy_bonus"></div>
<div style="font-size:11px; color:#a855f7; margin-top:2px;"
x-text="'✨ 魅力 +' + rings[0].charm_bonus"></div>
</div>
</template>
</div>
{{-- ── 婚礼档位选择与费用提示面板 ── --}}
@php
$activeTiers = \App\Models\WeddingTier::where('is_active', true)->orderBy('amount')->get();
@endphp
<div style="margin-bottom:14px; text-align:left;">
<div style="display:flex; align-items:center; gap:8px; margin-bottom:8px;">
<div
style="width:3px; height:14px; background:linear-gradient(#f59e0b,#d97706); border-radius:2px;">
</div>
<span style="font-size:12px; font-weight:700; color:#4b5563;">预设婚礼档位</span>
</div>
<select x-model="selectedTierId"
style="width:100%; padding:8px 10px; border-radius:8px; border:1px solid #d1d5db; background:#fff; font-size:13px; color:#1f2937; margin-bottom:10px;">
<option value="">(不举办撒红包婚礼)</option>
<template x-for="tier in tiers" :key="tier.id">
<option :value="tier.id" x-text="`${tier.icon} ${tier.name} (🪙 ${tier.amount})`">
</option>
</template>
</select>
<div x-show="selectedTier" x-transition
style="border-radius:12px; padding:12px 14px; font-size:12px; line-height:1.7; transition:all .2s;"
:style="canAfford ? 'background:#f0fdf4; border:1.5px solid #bbf7d0;' :
'background:#fef2f2; border:1.5px solid #fecaca;'">
<div style="font-weight:700; margin-bottom:4px;"
:style="canAfford ? 'color:#15803d' : 'color:#dc2626'"
x-text="canAfford ? '✅ 您的金币足以预定该婚礼' : '⚠️ 金币不足,请降低档位或准备金币'">
</div>
<div style="color:#6b7280;">
婚礼预冻结:<strong style="color:#f43f5e;"
x-text="'🪙 ' + (selectedTier ? Number(selectedTier.amount).toLocaleString() : 0)"></strong>
金币
</div>
<div :style="canAfford ? 'color:#15803d' : 'color:#dc2626'">
当前余额:<strong>🪙 {{ number_format($user->jjb) }}</strong> 金币
</div>
<div style="color:#9ca3af; font-size:11px; margin-top:4px;">需男方独自承担预冻结金币,对方同意后即刻举行。被拒则全额退回!
</div>
</div>
</div>
{{-- 底部按钮:样式修复并参照大卡片弹窗 --}}
<div style="display:flex; gap:12px;">
<button x-on:click="close()"
style="flex:1; padding:10px 0; border-radius:8px; font-size:13px; font-weight:bold;
cursor:pointer; border:none; background:#f1f5f9; color:#6b7280;
transition:background .15s;"
onmouseover="this.style.background='#e2e8f0'" onmouseout="this.style.background='#f1f5f9'">
取消
</button>
<button x-on:click="doPropose()"
:disabled="sending || !selectedRing || rings.length === 0 || !canAfford"
style="flex:1; padding:10px 0; border-radius:8px; font-size:13px; font-weight:bold; border:none; transition:all .2s;"
:style="(sending || !selectedRing || rings.length === 0 || !canAfford) ? { background: '#f1f5f9',
color: '#94a3b8', cursor: 'not-allowed', boxShadow: 'none' } : {
background: 'linear-gradient(135deg,#be185d,#f43f5e,#ec4899)',
color: '#fff',
cursor: 'pointer',
boxShadow: '0 4px 12px rgba(244,63,94,0.3)'
}">
<span x-text="sending ? '💌 发送中…' : '💍 确认求婚'"></span>
</button>
</div>
</div>
</div>
</div>
</div>
</div>
{{-- ═══════════ 2. 收到求婚弹窗 ═══════════ --}}
<div id="marriage-incoming-modal" x-data="marriageIncomingModal()" x-show="show" x-cloak>
<div
style="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()" x-show="show" x-cloak>
<div x-transition
style="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()" x-show="show" x-cloak>
<div
style="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()" x-show="show" x-cloak>
<div x-transition
style="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,
tiers: @json(\App\Models\WeddingTier::where('is_active', true)->orderBy('amount')->get()),
selectedTierId: '',
loading: false,
sending: false,
error: '',
get selectedTier() {
if (!this.selectedTierId) return null;
return this.tiers.find(t => t.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 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,
wedding_tier_id: this.selectedTierId || null,
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) {
// 直接打开当前页面的商店浮窗toolbar 中的 openShopModal
if (typeof window.openShopModal === 'function') {
window.openShopModal();
}
}
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_username,
partner_username
} = e.detail;
if (typeof appendSystemMessage === 'function') {
appendSystemMessage(`💔 ${user_username} 与 ${partner_username} 解除了婚姻关系。`);
}
});
/** 页面加载完成后初始化私人频道监听 */
document.addEventListener('DOMContentLoaded', () => {
const userId = window.chatContext?.userId;
if (userId && typeof window.initMarriagePrivateChannel === 'function') {
// 延迟初始化,确保 Echo 已就绪
setTimeout(() => window.initMarriagePrivateChannel(userId), 1500);
}
});
</script>