- 完成对 scripts.blade.php 中非核心业务逻辑(钓鱼游戏、AI机器人、系统全局公告)的深度抽象隔离 - 修复抢红包逻辑中 setInterval 缺失时间参数(1000)引发浏览器前端主线程挂起的重度阻塞问题 - 修复 lottery-panel 组件结尾漏写 </div> 导致的连锁级渲染树崩溃(该崩溃导致红包节点被意外当作隐藏后代节点渲染,造成彻底不可见) - 对相关模板规范代码结构,执行 Laravel Pint 格式化并提交
1473 lines
74 KiB
PHP
1473 lines
74 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()" 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, // 当前对方婚姻/求婚记录 ID(accept/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>
|