Files
chatroom/resources/views/chat/partials/marriage-modals.blade.php
lkddi bfb1a3bca4 重构(chat): 聊天室 Partials 第二阶段分类拆分及修复红包弹窗隐藏 Bug
- 完成对 scripts.blade.php 中非核心业务逻辑(钓鱼游戏、AI机器人、系统全局公告)的深度抽象隔离
- 修复抢红包逻辑中 setInterval 缺失时间参数(1000)引发浏览器前端主线程挂起的重度阻塞问题
- 修复 lottery-panel 组件结尾漏写 </div> 导致的连锁级渲染树崩溃(该崩溃导致红包节点被意外当作隐藏后代节点渲染,造成彻底不可见)
- 对相关模板规范代码结构,执行 Laravel Pint 格式化并提交
2026-03-09 11:30:11 +08:00

1473 lines
74 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" required
style="width:100%; padding:8px 10px; border-radius:8px; border:1px solid #f43f5e; background:#fff; font-size:13px; color:#1f2937; margin-bottom:10px;">
<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 style="font-size:11px; color:#9ca3af; margin-top:-6px; margin-bottom:8px;">🎊
必须选择婚礼档位,婚礼红包会撒给在场所有人!</div>
<div x-show="selectedTier" x-transition
style="display:none; 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>
<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>
{{-- ═══════════ 3.5. 离婚全屏公告(阴郁深色风格 + 雷雨特效) ═══════════ --}}
<div id="marriage-divorced-modal" x-data="marriageDivorcedModal()" x-show="show" x-cloak>
<div x-transition
style="position:fixed; inset:0; background:rgba(10,14,21,.82);
z-index:9900; display:flex; align-items:center; justify-content:center;">
<div
style="width:460px; max-width:95vw; background:linear-gradient(160deg,#1e2433,#252d3a);
border:1px solid rgba(100,116,140,.25);
border-radius:24px; box-shadow:0 24px 80px rgba(0,0,0,.7); overflow:hidden; text-align:center;">
{{-- 头部 --}}
<div
style="padding:32px 24px 20px;
background:linear-gradient(160deg,#2d3547,#1e2736);">
{{-- 断裂心形动效 --}}
<div
style="font-size:52px; margin-bottom:12px; line-height:1; position:relative; display:inline-block;">
<span style="display:inline-block; animation:divorce-shake 0.6s ease 0.3s both;">💔</span>
</div>
<div
style="color:#94a3b8; font-weight:bold; font-size:13px; letter-spacing:3px; text-transform:uppercase; margin-bottom:10px;">
缘尽于此
</div>
<div style="color:#e2e8f0; font-weight:bold; font-size:18px; margin-bottom:6px;"
x-text="announcement"></div>
<div style="color:#64748b; font-size:12px;" x-text="subText"></div>
</div>
{{-- 底部按钮 --}}
<div style="padding:18px 24px;">
<button x-on:click="close()"
style="padding:10px 40px; background:rgba(100,116,140,.15);
border:1px solid rgba(100,116,140,.3); border-radius:10px;
font-size:13px; color:#94a3b8; cursor:pointer; transition:all .2s;"
onmouseover="this.style.background='rgba(100,116,140,.25)'"
onmouseout="this.style.background='rgba(100,116,140,.15)'">
知道了
</button>
</div>
</div>
</div>
</div>
<style>
@keyframes divorce-shake {
0% {
transform: rotate(0deg) scale(1);
}
20% {
transform: rotate(-8deg) scale(1.1);
}
40% {
transform: rotate(6deg) scale(1.05);
}
60% {
transform: rotate(-4deg) scale(1.1);
}
80% {
transform: rotate(3deg) scale(1);
}
100% {
transform: rotate(0deg) scale(1);
}
}
</style>
{{-- ═══════════ 3.5. 发起离婚确认弹窗(发起方专用 + 后果说明) ═══════════ --}}
<div id="divorce-confirm-modal" x-data="divorceConfirmModal()" x-show="show" x-cloak>
<div x-transition
style="position:fixed; inset:0; background:rgba(10,14,21,.87); backdrop-filter:blur(3px);
z-index:9925; display:flex; align-items:center; justify-content:center; padding:16px;">
<div
style="width:480px; max-width:95vw; background:#1a1f2e; border:1px solid rgba(100,116,140,.3);
border-radius:24px; box-shadow:0 32px 80px rgba(0,0,0,.8); overflow:hidden;">
{{-- 头部 --}}
<div
style="padding:24px 28px 16px; background:linear-gradient(160deg,#252d3a,#1e2736); text-align:center;">
<div style="font-size:44px; margin-bottom:10px;">📄</div>
<div style="color:#f1f5f9; font-weight:bold; font-size:18px; margin-bottom:4px;">发起协议离婚申请</div>
<div style="color:#64748b; font-size:12px;">请仔细阅读以下后果,确认后再提交申请</div>
</div>
<div style="padding:16px 24px;">
{{-- 对方同意:协议离婚 --}}
<div
style="background:rgba(100,116,140,.08); border:1px solid rgba(100,116,140,.25); border-radius:12px; padding:12px 16px; margin-bottom:10px;">
<div style="color:#94a3b8; font-weight:bold; font-size:13px; margin-bottom:6px;"> 若对方同意(协议离婚)
</div>
<ul style="color:#94a3b8; font-size:12px; line-height:2; margin:0; padding-left:20px;">
<li>婚姻关系<strong style="color:#e2e8f0;">立即解除</strong>,亲密度清零</li>
<li>双方各被扣除 <strong style="color:#fca5a5;" x-text="mutualPenalty + ' 点魅力值'"></strong></li>
<li>双方进入 <strong style="color:#e2e8f0;" x-text="mutualCooldown + ' 天'"></strong>
离婚冷静期,期间无法再次结婚</li>
</ul>
</div>
{{-- 对方拒绝:强制离婚,自己赔钱 --}}
<div
style="background:rgba(239,68,68,.08); border:1px solid rgba(239,68,68,.3); border-radius:12px; padding:12px 16px; margin-bottom:16px;">
<div style="color:#fca5a5; font-weight:bold; font-size:13px; margin-bottom:6px;">⚠️
若对方拒绝(视为强制离婚,后果由你承担)</div>
<ul style="color:#94a3b8; font-size:12px; line-height:2; margin:0; padding-left:20px;">
<li>婚姻关系<strong style="color:#fca5a5;">立即强制解除</strong></li>
<li><strong style="color:#fca5a5;">你将被扣除 <span x-text="forcedPenalty"></span>
点魅力值</strong>作为惩罚</li>
<li><strong style="color:#fca5a5;">你当前一半的金币将赔偿给对方</strong></li>
<li>你进入 <strong style="color:#fca5a5;" x-text="forcedCooldown + ' 天'"></strong>
强制离婚冷静期,期间无法再次结婚</li>
</ul>
</div>
{{-- 操作按钮 --}}
<div style="display:flex; gap:10px; justify-content:center;">
<button x-on:click="doConfirm()" :disabled="acting"
style="flex:1; padding:12px 0; border-radius:12px; border:none; font-size:14px; font-weight:bold; cursor:pointer; transition:all .2s;
background:linear-gradient(135deg,#475569,#334155); color:#e2e8f0; box-shadow:0 4px 12px rgba(0,0,0,.3);"
onmouseover="this.style.opacity='0.85'" onmouseout="this.style.opacity='1'">
<span x-text="acting ? '发送中…' : '📄 确认发起申请'"></span>
</button>
<button x-on:click="close()" :disabled="acting"
style="flex:0 0 auto; padding:12px 24px; border-radius:12px; font-size:13px; cursor:pointer; transition:all .2s;
background:rgba(100,116,140,.12); border:1px solid rgba(100,116,140,.3); color:#64748b;"
onmouseover="this.style.background='rgba(100,116,140,.22)'"
onmouseout="this.style.background='rgba(100,116,140,.12)'">
取消
</button>
</div>
<div x-show="error" x-transition
style="display:none; margin-top:10px; background:rgba(239,68,68,.1); border:1px solid rgba(239,68,68,.3); border-radius:8px; padding:8px 12px; font-size:12px; color:#fca5a5; text-align:center;"
x-text="error"></div>
</div>
</div>
</div>
</div>
{{-- ═══════════ 3.6. 离婚申请通知弹窗(三选按钮 + 后果说明) ═══════════ --}}
<div id="divorce-request-modal" x-data="divorceRequestModal()" x-show="show" x-cloak>
<div x-transition
style="position:fixed; inset:0; background:rgba(10,14,21,.85); backdrop-filter:blur(3px);
z-index:9920; display:flex; align-items:center; justify-content:center; padding:16px;">
<div
style="width:480px; max-width:95vw; background:#1a1f2e; border:1px solid rgba(100,116,140,.3);
border-radius:24px; box-shadow:0 32px 80px rgba(0,0,0,.8); overflow:hidden;">
{{-- 头部 --}}
<div
style="padding:28px 28px 20px; background:linear-gradient(160deg,#252d3a,#1e2736); text-align:center;">
<div style="font-size:48px; margin-bottom:12px; animation:divorce-shake 0.7s ease 0.2s both;">💔</div>
<div style="color:#f1f5f9; font-weight:bold; font-size:19px; margin-bottom:6px;">收到离婚申请</div>
<div style="color:#94a3b8; font-size:14px; line-height:1.6;" x-text="initiatorName + ' 向你提出了协议离婚申请。'">
</div>
<div style="color:#64748b; font-size:12px; margin-top:4px;">请仔细阅读以下后果说明,做出你的选择:</div>
</div>
{{-- 后果说明区 --}}
<div style="padding:16px 24px;">
{{-- 同意后果 --}}
<div
style="background:rgba(239,68,68,.08); border:1px solid rgba(239,68,68,.25); border-radius:12px; padding:12px 16px; margin-bottom:10px;">
<div style="display:flex; align-items:center; gap:8px; margin-bottom:6px;">
<span
style="background:#ef4444; border-radius:50%; width:22px; height:22px; display:flex; align-items:center; justify-content:center; font-size:12px; font-weight:bold; color:#fff; flex-shrink:0;"></span>
<span style="color:#fca5a5; font-weight:bold; font-size:13px;">同意离婚</span>
</div>
<ul style="color:#94a3b8; font-size:12px; line-height:2; margin:0; padding-left:30px;">
<li>婚姻关系<strong style="color:#fca5a5;">立即解除</strong>,所有亲密度清零</li>
<li>双方各被扣除 <strong style="color:#fca5a5;" x-text="mutualPenalty + ' 点魅力值'">? 点魅力值</strong>
</li>
<li>双方都将进入<strong style="color:#fca5a5;">离婚冷静期</strong>,期间无法再次结婚</li>
</ul>
</div>
{{-- 拒绝后果 --}}
<div
style="background:rgba(234,179,8,.08); border:1px solid rgba(234,179,8,.25); border-radius:12px; padding:12px 16px; margin-bottom:10px;">
<div style="display:flex; align-items:center; gap:8px; margin-bottom:6px;">
<span
style="background:#ca8a04; border-radius:50%; width:22px; height:22px; display:flex; align-items:center; justify-content:center; font-size:12px; font-weight:bold; color:#fff; flex-shrink:0;"></span>
<span style="color:#fde68a; font-weight:bold; font-size:13px;">不同意(强制离婚)</span>
</div>
<ul style="color:#94a3b8; font-size:12px; line-height:2; margin:0; padding-left:30px;">
<li>视同<strong style="color:#fde68a;">强制离婚</strong>,婚姻<strong
style="color:#fde68a;">立即解除</strong></li>
<li>申请方被扣除大量魅力值作为<strong style="color:#fde68a;">惩罚</strong></li>
<li>申请方当前<strong style="color:#fde68a;">一半金币赔偿给你</strong>(入账到你账户)</li>
<li>申请方进入<strong style="color:#fde68a;">强制离婚冷静期</strong>,期间无法再次结婚</li>
</ul>
</div>
{{-- 取消(稍后处理) --}}
<div
style="background:rgba(100,116,140,.06); border:1px solid rgba(100,116,140,.2); border-radius:12px; padding:10px 16px; margin-bottom:16px;">
<div style="display:flex; align-items:center; gap:8px; margin-bottom:4px;">
<span
style="background:#475569; border-radius:50%; width:22px; height:22px; display:flex; align-items:center; justify-content:center; font-size:12px; font-weight:bold; color:#fff; flex-shrink:0;"></span>
<span style="color:#94a3b8; font-weight:bold; font-size:13px;">稍后决定</span>
</div>
<ul style="color:#64748b; font-size:12px; line-height:2; margin:0; padding-left:30px;">
<li>关闭此弹窗,<strong style="color:#94a3b8;">暂不做决定</strong></li>
<li>下次登录或刷新页面时<strong style="color:#94a3b8;">仍会再次弹出</strong>提示</li>
<li>72 小时内若不处理,系统将<strong style="color:#94a3b8;">自动视为对方强制离婚</strong></li>
</ul>
</div>
{{-- 三个按钮 --}}
<div style="display:flex; gap:10px; justify-content:center;">
{{-- 同意 --}}
<button x-on:click="doAgree()" :disabled="acting"
style="flex:1; padding:12px 0; border-radius:12px; border:none; font-size:14px; font-weight:bold; cursor:pointer; transition:all .2s;
background:linear-gradient(135deg,#dc2626,#b91c1c); color:#fff; box-shadow:0 4px 12px rgba(220,38,38,.35);"
onmouseover="this.style.opacity='0.85'" onmouseout="this.style.opacity='1'">
<span x-text="acting ? '处理中…' : '✓ 同意离婚'"></span>
</button>
{{-- 拒绝 --}}
<button x-on:click="doReject()" :disabled="acting"
style="flex:1; padding:12px 0; border-radius:12px; border:none; font-size:14px; font-weight:bold; cursor:pointer; transition:all .2s;
background:linear-gradient(135deg,#d97706,#b45309); color:#fff; box-shadow:0 4px 12px rgba(217,119,6,.3);"
onmouseover="this.style.opacity='0.85'" onmouseout="this.style.opacity='1'">
<span x-text="acting ? '处理中…' : '✕ 拒绝离婚'"></span>
</button>
{{-- 取消 --}}
<button x-on:click="close()" :disabled="acting"
style="flex:0 0 auto; padding:12px 20px; border-radius:12px; font-size:13px; cursor:pointer; transition:all .2s;
background:rgba(100,116,140,.15); border:1px solid rgba(100,116,140,.3); color:#94a3b8;"
onmouseover="this.style.background='rgba(100,116,140,.25)'"
onmouseout="this.style.background='rgba(100,116,140,.15)'">
稍后
</button>
</div>
<div x-show="error" x-transition
style="display:none; background:rgba(239,68,68,.1); border:1px solid rgba(239,68,68,.3); border-radius:8px; padding:8px 12px; margin-top:12px; font-size:12px; color:#fca5a5; text-align:center;"
x-text="error"></div>
</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 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,#c0392b,#e74c3c,#c0392b);
border-radius:20px; overflow:hidden; text-align:center;
box-shadow:0 24px 80px rgba(231,76,60,.65), 0 0 0 1px rgba(255,210,100,.25);
border:1px solid rgba(255,210,100,.3);">
<div style="padding:28px 20px 12px;">
<div style="font-size:56px; margin-bottom:8px; animation:pulse 1.5s infinite;">🧧</div>
<div style="color:#fff8dc; font-weight:bold; font-size:18px; margin-bottom:4px; text-shadow:0 1px 4px rgba(0,0,0,.3);"
x-text="title"></div>
<div style="color:rgba(255,248,220,.75); font-size:12px; margin-bottom:8px;" x-text="subTitle"></div>
{{-- 未领取 --}}
<div x-show="!claimed" style="display:none;">
<div style="color:rgba(255,248,220,.55); font-size:11px; margin-bottom:20px;">
红包有效期 <strong style="color:#fcd34d;">24小时</strong>,过期自动消失
</div>
{{-- 圆形领取按钮(仿「開」按钮,全样式写入 :style 避免 Alpine 覆盖) --}}
<div style="display:flex; justify-content:center; margin-bottom:20px;">
<button x-on:click="doClaim()" :disabled="claiming"
onmouseover="if(!this.disabled)this.style.transform='scale(1.08)'"
onmouseout="this.style.transform=''"
:style="claiming
?
'width:130px; height:130px; border-radius:50%; border:none; cursor:not-allowed; display:flex; align-items:center; justify-content:center; flex-shrink:0; box-sizing:border-box; transition:all .25s; font-size:28px; font-weight:900; letter-spacing:2px; background:#c8b89a; color:rgba(100,50,20,.45); box-shadow:none;' :
'width:130px; height:130px; border-radius:50%; border:none; cursor:pointer; display:flex; align-items:center; justify-content:center; flex-shrink:0; box-sizing:border-box; transition:all .25s; font-size:28px; font-weight:900; letter-spacing:2px; background:#f5e6c8; color:#8b3a1a; box-shadow:0 6px 28px rgba(0,0,0,.35), inset 0 -4px 10px rgba(139,58,26,.12); animation:grab-btn-pulse 1.8s ease-in-out infinite;'">
<span x-text="claiming ? '…' : '领取'"></span>
</button>
</div>
</div>
{{-- 已领取 --}}
<div x-show="claimed" style="display:none; padding-bottom:8px;">
<div style="font-size:40px; margin-bottom:6px; color:#fcd34d; font-weight:bold;"
x-text="'+' + claimedAmount.toLocaleString() + ' 金'"></div>
<div style="color:#fff8dc; font-size:13px;">🎉 恭喜你领取了红包!</div>
<div style="color:rgba(255,248,220,.6); font-size:11px; margin-top:4px;">金币已自动到账</div>
</div>
</div>
{{-- 关闭按钮 --}}
<div style="padding:0 20px 24px; display:flex; justify-content:center;">
<button x-on:click="close()"
style="width:200px; padding:12px 0; background:rgba(0,0,0,.3); border:none; border-radius:30px;
font-size:13px; color:rgba(255,248,220,.8); cursor:pointer; letter-spacing:1px;
transition:background .15s;"
onmouseover="this.style.background='rgba(0,0,0,.48)'"
onmouseout="this.style.background='rgba(0,0,0,.3)'">
<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>
/**
* 全局辅助:向聊天主窗口追加一条婚姻系统公告(支持 HTML 内容,如按钮)
* 使用 innerHTML 而非 textContent以支持内嵌的领取按钮等交互元素。
*/
window.appendSystemMessage = function(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;
};
/**
* 求婚弹窗组件
*/
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: @json(\App\Models\WeddingTier::where('is_active', true)->orderBy('amount')->value('id') ?? ''), // 默认选最小档位
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;
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 me = window.chatContext.username;
this.isNewlywed = (groomName === me || brideName === me);
this.show = true;
// 播放婚礼专属双倍礼花特效(全员)
if (window.EffectManager) {
window.EffectManager.play('wedding-fireworks');
}
},
close() {
this.show = false;
},
openWeddingSetup() {
openWeddingSetupModal(this.marriageId);
}
};
}
/**
* 离婚全屏公告弹窗组件(阴郁深色风格,雷雨双特效)
*/
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;
// 先播放雷电,再叠加下雨
if (window.EffectManager) {
window.EffectManager.play('lightning');
// 雷电通常持续约3秒3.5秒后再起下雨(雨声会压住)
setTimeout(() => {
if (window.EffectManager) {
window.EffectManager.play('rain');
}
}, 3500);
}
},
close() {
this.show = false;
}
};
}
/**
* 发起离婚确认弹窗(发起方专用:展示双方结果 + 实时惩罚值)
*/
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 res = 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 res.json();
this.close();
if (data.ok) {
window.chatDialog?.alert(data.message, '申请已发出 📩', '#6b7280');
} else {
window.chatDialog?.alert(data.message || '操作失败', '错误', '#dc2626');
}
} catch (e) {
this.error = '网络请求失败,请重试。';
} finally {
this.acting = false;
}
}
};
}
/**
* 离婚申请通知弹窗(被申请方专用:三选 + 真实惩罚值)
*/
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;
},
/** 同意:协议离婚,双方各扣 mutualPenalty 魅力 */
async doAgree() {
if (this.acting) return;
this.acting = true;
this.error = '';
try {
const res = 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 res.json();
this.close();
window.chatDialog?.alert(data.message, data.ok ? '操作完成' : '失败', data.ok ? '#6b7280' :
'#cc4444');
} catch (e) {
this.error = '网络请求失败,请重试。';
} finally {
this.acting = false;
}
},
/** 不同意:视为强制离婚,申请方扣魅力 + 赔一半金币 */
async doReject() {
if (this.acting) return;
this.acting = true;
this.error = '';
try {
const res = 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 res.json();
this.close();
window.chatDialog?.alert(data.message, data.ok ? '已处理' : '失败', data.ok ? '#d97706' : '#cc4444');
} catch (e) {
this.error = '网络请求失败,请重试。';
} finally {
this.acting = false;
}
}
};
}
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 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,
payer_type: this.payBy,
ceremony_type: 'immediate',
room_id: window.chatContext.roomId,
};
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('🎊 婚礼已开始!红包正在分发给在线用户…', '设置成功', '#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;
// 兼容两种字段命名groom_name/bride_name 或 user.username/partner.username
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 {
// 正确路由:/wedding/ceremony/{ceremonyId}/claim
const res = 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 res.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;
}
};
}
// ───────── 全局入口函数 ─────────────────────────────────
/**
* 打开求婚弹窗(从名片按钮调用)。
* 先检查背包是否有戒指:
* - 有 → 直接开弹窗(戒指列表已预加载,无需二次请求)
* - 无 → 提示用户前往商店购买
*/
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 groomName = detail.user?.username ?? detail.groom_name ?? '??';
const brideName = detail.partner?.username ?? detail.bride_name ?? '??';
// 追加公屏消息
if (typeof appendSystemMessage === 'function') {
appendSystemMessage(`💑 ${groomName} 与 ${brideName} 喜结连理!`);
}
// 弹出全屏公告fireworks 效果)
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 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;
// 将 detail 存入全局 Map避免 onclick 属性内嵌 JSON 被双引号破坏
if (!window._weddingEnvelopes) window._weddingEnvelopes = {};
window._weddingEnvelopes[ceremonyId] = detail;
// 公屏追加带按钮的系统消息(按钮通过 ceremonyId 引用全局 Map
if (typeof appendSystemMessage === 'function') {
const claimBtn = `<button onclick="(function(){var d=window._weddingEnvelopes[${ceremonyId}];var el=document.getElementById('wedding-envelope-modal');if(el&&d)Alpine.$data(el).open(d);})()"
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>`;
appendSystemMessage(
`${tierIcon} ${groomName} 与 ${brideName} 举办了【${tierName}】!总金额 💰${amount} 金币,快来抢红包!${claimBtn}`
);
}
// 同时弹出全屏红包弹窗
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 detail = e.detail;
const el = document.getElementById('divorce-request-modal');
if (el) Alpine.$data(el).open(detail);
});
/** 红包领取成功通知(私人频道) */
window.addEventListener('chat:envelope-claimed', (e) => {
const {
amount
} = e.detail;
window.chatDialog?.alert(`+${amount.toLocaleString()} 金币已到账 🎉`, '红包到手!', '#f59e0b');
});
window.addEventListener('chat:marriage-divorced', (e) => {
const detail = e.detail;
const userName = detail.user_username ?? detail.user?.username ?? '??';
const partnerName = detail.partner_username ?? detail.partner?.username ?? '??';
// 追加公屏文字
if (typeof appendSystemMessage === 'function') {
appendSystemMessage(`💔 ${userName} 与 ${partnerName} 解除了婚姻关系。`);
}
// 触发全屏离婚公告弹窗(暗色阴郁风格 + 雷雨特效)
const modal = document.getElementById('marriage-divorced-modal')?._x_dataStack?.[0];
if (modal && typeof modal.open === 'function') modal.open(detail);
});
/** 页面加载完成后初始化私人频道监听 */
document.addEventListener('DOMContentLoaded', () => {
const userId = window.chatContext?.userId;
if (userId && typeof window.initMarriagePrivateChannel === 'function') {
// 延迟初始化,确保 Echo 已就绪
setTimeout(() => window.initMarriagePrivateChannel(userId), 1500);
}
// ── 页面刷新后恢复婚礼红包领取按钮 ─────────────────────────
// 延迟 2 秒以确保聊天框和 Alpine 均已完成初始化
setTimeout(async () => {
try {
const res = await fetch('/wedding/pending-envelopes', {
headers: {
'Accept': 'application/json'
}
});
const data = await res.json();
if (!res.ok || !data.envelopes?.length) return;
// 初始化全局缓存
if (!window._weddingEnvelopes) window._weddingEnvelopes = {};
data.envelopes.forEach(env => {
const ceremonyId = env.ceremony_id;
// 注入 detail 到全局 Map供领取弹窗使用
window._weddingEnvelopes[ceremonyId] = {
ceremony_id: ceremonyId,
total_amount: env.total_amount,
tier_name: env.tier_name,
tier_icon: env.tier_icon,
user: {
username: env.groom
},
partner: {
username: env.bride
},
};
// 在包厢窗口追加提示 + 领取按钮
if (typeof appendSystemMessage === 'function') {
const claimBtn = `<button onclick="(function(){var d=window._weddingEnvelopes[${ceremonyId}];var el=document.getElementById('wedding-envelope-modal');if(el&&d)Alpine.$data(el).open(d);})()"
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>`;
appendSystemMessage(
`⚠️ 您有来自 ${env.tier_icon} ${env.groom} 与 ${env.bride}【${env.tier_name}】的婚礼红包未领取!${claimBtn}`
);
}
});
} catch (e) {
console.warn('[婚礼红包] 恢复待领取按钮失败', e);
}
}, 2000);
});
</script>