2026-03-01 15:31:07 +08:00
|
|
|
|
{{--
|
|
|
|
|
|
文件功能:婚姻系统前端弹窗组件集合
|
|
|
|
|
|
|
|
|
|
|
|
包含:
|
|
|
|
|
|
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. 求婚弹窗 ═══════════ --}}
|
2026-03-01 17:17:14 +08:00
|
|
|
|
<div id="marriage-propose-modal" x-data="marriageProposeModal()" x-show="show" x-cloak>
|
2026-03-01 17:21:48 +08:00
|
|
|
|
<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;"
|
2026-03-01 15:31:07 +08:00
|
|
|
|
x-on:click.self="close()">
|
2026-03-01 17:21:48 +08:00
|
|
|
|
<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);">
|
|
|
|
|
|
|
|
|
|
|
|
{{-- ───── 封面区域 ────── --}}
|
2026-03-01 15:31:07 +08:00
|
|
|
|
<div
|
2026-03-01 17:21:48 +08:00
|
|
|
|
style="background:linear-gradient(145deg,#be185d 0%,#f43f5e 45%,#fb7185 80%,#fda4af 100%);
|
|
|
|
|
|
padding:32px 24px 24px; text-align:center; position:relative;">
|
|
|
|
|
|
{{-- 关闭按钮 --}}
|
2026-03-01 15:31:07 +08:00
|
|
|
|
<button x-on:click="close()"
|
2026-03-01 17:21:48 +08:00
|
|
|
|
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>
|
2026-03-01 15:31:07 +08:00
|
|
|
|
</div>
|
2026-03-01 17:21:48 +08:00
|
|
|
|
|
|
|
|
|
|
{{-- ───── 内容区域 ────── --}}
|
|
|
|
|
|
<div style="background:#fff; padding:24px;">
|
|
|
|
|
|
|
2026-03-01 15:31:07 +08:00
|
|
|
|
{{-- 错误提示 --}}
|
2026-03-01 17:21:48 +08:00
|
|
|
|
<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;"
|
2026-03-01 15:31:07 +08:00
|
|
|
|
x-text="error"></div>
|
|
|
|
|
|
|
|
|
|
|
|
{{-- 加载中 --}}
|
2026-03-01 17:21:48 +08:00
|
|
|
|
<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>
|
2026-03-01 15:31:07 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-03-01 17:28:28 +08:00
|
|
|
|
{{-- 戒指展示区 --}}
|
2026-03-01 15:31:07 +08:00
|
|
|
|
<div x-show="!loading">
|
2026-03-01 17:21:48 +08:00
|
|
|
|
|
|
|
|
|
|
{{-- 区域标题 --}}
|
|
|
|
|
|
<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>
|
2026-03-01 17:28:28 +08:00
|
|
|
|
<span style="font-size:13px; font-weight:700; color:#1f2937;">赠送的求婚戒</span>
|
2026-03-01 17:21:48 +08:00
|
|
|
|
<span style="font-size:11px; color:#d1d5db;">戒指消耗后不退,拒绝则遗失</span>
|
2026-03-01 15:31:07 +08:00
|
|
|
|
</div>
|
2026-03-01 17:21:48 +08:00
|
|
|
|
|
|
|
|
|
|
{{-- 无戒指提示 --}}
|
2026-03-01 15:31:07 +08:00
|
|
|
|
<div x-show="rings.length === 0"
|
2026-03-01 17:21:48 +08:00
|
|
|
|
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>
|
2026-03-01 15:31:07 +08:00
|
|
|
|
</div>
|
2026-03-01 17:21:48 +08:00
|
|
|
|
|
2026-03-01 17:28:28 +08:00
|
|
|
|
{{-- 戒指展示(居中,只展示第一枚,不可选) --}}
|
|
|
|
|
|
<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>
|
2026-03-01 15:31:07 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</div>
|
2026-03-01 17:21:48 +08:00
|
|
|
|
|
2026-03-01 17:22:57 +08:00
|
|
|
|
{{-- ── 婚礼费用提示面板 ── --}}
|
|
|
|
|
|
@php
|
|
|
|
|
|
$minWedding = (int) \App\Models\WeddingTier::where('is_active', true)
|
|
|
|
|
|
->orderBy('amount')
|
|
|
|
|
|
->value('amount');
|
|
|
|
|
|
@endphp
|
|
|
|
|
|
@if ($minWedding > 0)
|
|
|
|
|
|
<div style="margin-bottom:14px;">
|
|
|
|
|
|
@php $canAfford = ($user->jjb >= $minWedding); @endphp
|
|
|
|
|
|
<div
|
|
|
|
|
|
style="border-radius:12px; padding:12px 14px; font-size:12px; line-height:1.7;
|
|
|
|
|
|
background:{{ $canAfford ? '#f0fdf4' : '#fef2f2' }};
|
|
|
|
|
|
border:1.5px solid {{ $canAfford ? '#bbf7d0' : '#fecaca' }};">
|
|
|
|
|
|
<div
|
|
|
|
|
|
style="font-weight:700; color:{{ $canAfford ? '#15803d' : '#dc2626' }}; margin-bottom:4px;">
|
|
|
|
|
|
{{ $canAfford ? '✅ 您的金币可以举办婚礼' : '⚠️ 金币可能不足以举办婚礼' }}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div style="color:#6b7280;">
|
|
|
|
|
|
结婚最低花费:<strong style="color:#f43f5e;">🪙 {{ number_format($minWedding) }}</strong>
|
|
|
|
|
|
金币
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div style="color:#{{ $canAfford ? '15803d' : 'dc2626' }};">
|
|
|
|
|
|
当前余额:<strong>🪙 {{ number_format($user->jjb) }}</strong> 金币
|
|
|
|
|
|
</div>
|
|
|
|
|
|
@if (!$canAfford)
|
|
|
|
|
|
<div style="color:#9ca3af; font-size:11px; margin-top:4px;">可先求婚,婚礼设置时再准备金币</div>
|
|
|
|
|
|
@endif
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
@endif
|
|
|
|
|
|
|
2026-03-01 17:28:28 +08:00
|
|
|
|
{{-- 底部按钮:与好友名片按钮风格一致 --}}
|
|
|
|
|
|
<div style="display:flex; gap:6px;">
|
2026-03-01 17:24:29 +08:00
|
|
|
|
<button x-on:click="close()"
|
2026-03-01 17:28:28 +08:00
|
|
|
|
style="flex:1; padding:7px 10px; border-radius:5px; font-size:12px; font-weight:bold;
|
2026-03-01 17:24:29 +08:00
|
|
|
|
cursor:pointer; background:#f1f5f9; color:#6b7280; border:1px solid #d1d5db;
|
2026-03-01 17:28:28 +08:00
|
|
|
|
transition:opacity .15s;">
|
2026-03-01 17:24:29 +08:00
|
|
|
|
✕ 取消
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button x-on:click="doPropose()" :disabled="sending || !selectedRing || rings.length === 0"
|
2026-03-01 17:28:28 +08:00
|
|
|
|
style="flex:1; padding:7px 10px; border-radius:5px; font-size:12px; font-weight:bold;
|
|
|
|
|
|
cursor:pointer; border:none; transition:opacity .15s;"
|
2026-03-01 17:24:29 +08:00
|
|
|
|
:style="(sending || !selectedRing || rings.length === 0) ?
|
|
|
|
|
|
'background:#f1f5f9; color:#94a3b8; cursor:not-allowed;' :
|
2026-03-01 17:28:28 +08:00
|
|
|
|
'background:linear-gradient(135deg,#be185d,#f43f5e,#ec4899); color:#fff;'">
|
2026-03-01 17:24:29 +08:00
|
|
|
|
<span x-text="sending ? '💌 发送中…' : '💍 确认求婚'"></span>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
2026-03-01 17:21:48 +08:00
|
|
|
|
|
2026-03-01 15:31:07 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{{-- ═══════════ 2. 收到求婚弹窗 ═══════════ --}}
|
2026-03-01 17:17:14 +08:00
|
|
|
|
<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);
|
2026-03-01 15:31:07 +08:00
|
|
|
|
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. 结婚成功全屏公告 ═══════════ --}}
|
2026-03-01 17:17:14 +08:00
|
|
|
|
<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);
|
2026-03-01 15:31:07 +08:00
|
|
|
|
z-index:9900; display:flex; align-items:center; justify-content:center;">
|
|
|
|
|
|
<div
|
|
|
|
|
|
style="width:460px; max-width:95vw; background:#fff; border-radius:24px;
|
|
|
|
|
|
box-shadow:0 24px 80px rgba(244,63,94,.5); overflow:hidden; text-align:center;">
|
|
|
|
|
|
<div style="background:linear-gradient(135deg,#f43f5e,#ec4899,#a855f7); padding:28px 20px;">
|
|
|
|
|
|
<div style="font-size:50px; margin-bottom:8px;">💑</div>
|
|
|
|
|
|
<div style="color:#fff; font-weight:bold; font-size:20px; margin-bottom:4px;">
|
|
|
|
|
|
🎊 天作之合!
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div style="color:rgba(255,255,255,.9); font-size:15px;" x-text="announcement"></div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div style="padding:20px 24px;">
|
|
|
|
|
|
<div style="font-size:13px; color:#6b7280; margin-bottom:16px;" x-text="subText"></div>
|
|
|
|
|
|
{{-- 举办婚礼按钮(仅对新婚夫妻显示) --}}
|
|
|
|
|
|
<div x-show="isNewlywed"
|
|
|
|
|
|
style="display:none; display:flex; gap:10px; justify-content:center; margin-bottom:14px;">
|
|
|
|
|
|
<button x-on:click="openWeddingSetup(); close();"
|
|
|
|
|
|
style="padding:11px 24px; background:linear-gradient(135deg,#f59e0b,#d97706);
|
|
|
|
|
|
color:#fff; border:none; border-radius:12px; font-size:13px; font-weight:bold; cursor:pointer;">
|
|
|
|
|
|
🎊 立即举办婚礼!
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<button x-on:click="close()"
|
|
|
|
|
|
style="padding:10px 32px; background:#f1f5f9; border:none; border-radius:10px;
|
|
|
|
|
|
font-size:13px; color:#6b7280; cursor:pointer;">关闭</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{{-- ═══════════ 4. 婚礼设置弹窗 ═══════════ --}}
|
2026-03-01 17:17:14 +08:00
|
|
|
|
<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);
|
2026-03-01 15:31:07 +08:00
|
|
|
|
z-index:9820; display:flex; align-items:center; justify-content:center;">
|
|
|
|
|
|
<div
|
|
|
|
|
|
style="width:500px; max-width:95vw; background:#fff; border-radius:20px;
|
|
|
|
|
|
box-shadow:0 20px 60px rgba(245,158,11,.3); overflow:hidden;">
|
|
|
|
|
|
<div
|
|
|
|
|
|
style="background:linear-gradient(135deg,#f59e0b,#d97706); padding:16px 20px;
|
|
|
|
|
|
display:flex; align-items:center; justify-content:space-between;">
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<div style="color:#fff; font-weight:bold; font-size:17px;">🎊 举办婚礼庆典</div>
|
|
|
|
|
|
<div style="color:rgba(255,255,255,.85); font-size:12px; margin-top:2px;">宴请全场,赠送红包大礼</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<button x-on:click="close()"
|
|
|
|
|
|
style="background:rgba(255,255,255,.25); border:none; color:#fff;
|
|
|
|
|
|
width:30px; height:30px; border-radius:50%; cursor:pointer; font-size:18px;">×</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div style="padding:20px;">
|
|
|
|
|
|
<div x-show="error"
|
|
|
|
|
|
style="display:none; background:#fef2f2; border:1px solid #fecaca;
|
|
|
|
|
|
color:#dc2626; border-radius:8px; padding:10px 14px; font-size:12px; margin-bottom:14px;"
|
|
|
|
|
|
x-text="error"></div>
|
|
|
|
|
|
|
|
|
|
|
|
{{-- 档位选择 --}}
|
|
|
|
|
|
<div style="font-size:13px; font-weight:bold; color:#374151; margin-bottom:10px;">选择庆典档位</div>
|
|
|
|
|
|
<div x-show="loading" style="text-align:center; padding:16px; color:#f59e0b;">加载档位中…</div>
|
|
|
|
|
|
<div x-show="!loading"
|
|
|
|
|
|
style="display:grid; grid-template-columns:repeat(5,1fr); gap:8px; margin-bottom:16px;">
|
|
|
|
|
|
<template x-for="tier in tiers" :key="tier.id">
|
|
|
|
|
|
<div x-on:click="selectedTier = tier"
|
|
|
|
|
|
:style="selectedTier && selectedTier.id === tier.id ?
|
|
|
|
|
|
'border:2px solid #f59e0b; background:#fffbeb;' :
|
|
|
|
|
|
'border:2px solid #e2e8f0; background:#fff;'"
|
|
|
|
|
|
style="padding:10px 6px; border-radius:10px; cursor:pointer; text-align:center; transition:all .15s;">
|
|
|
|
|
|
<div style="font-size:22px;" x-text="tier.icon"></div>
|
|
|
|
|
|
<div style="font-size:11px; font-weight:bold; color:#374151; margin-top:3px;"
|
|
|
|
|
|
x-text="tier.name"></div>
|
|
|
|
|
|
<div style="font-size:11px; color:#f59e0b; font-weight:bold;"
|
|
|
|
|
|
x-text="tier.amount.toLocaleString() + ' 金'"></div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{{-- 支付方式 --}}
|
|
|
|
|
|
<div x-show="selectedTier" style="display:none;">
|
|
|
|
|
|
<div style="font-size:13px; font-weight:bold; color:#374151; margin-bottom:10px;">金币支付方式</div>
|
|
|
|
|
|
<div style="display:flex; gap:10px; margin-bottom:16px;">
|
|
|
|
|
|
<label
|
|
|
|
|
|
style="flex:1; display:flex; align-items:center; gap:8px; padding:10px;
|
|
|
|
|
|
border-radius:10px; cursor:pointer;"
|
|
|
|
|
|
:style="payBy === 'groom' ? 'border:2px solid #f59e0b; background:#fffbeb;' :
|
|
|
|
|
|
'border:2px solid #e2e8f0;'">
|
|
|
|
|
|
<input type="radio" x-model="payBy" value="groom" style="accent-color:#f59e0b;">
|
|
|
|
|
|
<span style="font-size:12px; font-weight:bold;">由我全额支付</span>
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<label
|
|
|
|
|
|
style="flex:1; display:flex; align-items:center; gap:8px; padding:10px;
|
|
|
|
|
|
border-radius:10px; cursor:pointer;"
|
|
|
|
|
|
:style="payBy === 'split' ? 'border:2px solid #f59e0b; background:#fffbeb;' :
|
|
|
|
|
|
'border:2px solid #e2e8f0;'">
|
|
|
|
|
|
<input type="radio" x-model="payBy" value="split" style="accent-color:#f59e0b;">
|
|
|
|
|
|
<span style="font-size:12px; font-weight:bold;">双方各出一半</span>
|
|
|
|
|
|
</label>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{{-- 时间选择 --}}
|
|
|
|
|
|
<div style="font-size:13px; font-weight:bold; color:#374151; margin-bottom:10px;">举办时间</div>
|
|
|
|
|
|
<div style="display:flex; gap:10px; margin-bottom:16px;">
|
|
|
|
|
|
<label
|
|
|
|
|
|
style="flex:1; display:flex; align-items:center; gap:8px; padding:10px;
|
|
|
|
|
|
border-radius:10px; cursor:pointer;"
|
|
|
|
|
|
:style="timing === 'now' ? 'border:2px solid #ec4899; background:#fff1f2;' :
|
|
|
|
|
|
'border:2px solid #e2e8f0;'">
|
|
|
|
|
|
<input type="radio" x-model="timing" value="now" style="accent-color:#ec4899;">
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<div style="font-size:12px; font-weight:bold;">🎊 立即举办</div>
|
|
|
|
|
|
<div style="font-size:10px; color:#9ca3af;">随机红包即刻发出</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<label
|
|
|
|
|
|
style="flex:1; display:flex; align-items:center; gap:8px; padding:10px;
|
|
|
|
|
|
border-radius:10px; cursor:pointer;"
|
|
|
|
|
|
:style="timing === 'scheduled' ? 'border:2px solid #ec4899; background:#fff1f2;' :
|
|
|
|
|
|
'border:2px solid #e2e8f0;'">
|
|
|
|
|
|
<input type="radio" x-model="timing" value="scheduled" style="accent-color:#ec4899;">
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<div style="font-size:12px; font-weight:bold;">⏰ 定时举办</div>
|
|
|
|
|
|
<div style="font-size:10px; color:#9ca3af;">选一个吉时</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</label>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div x-show="timing === 'scheduled'" style="display:none; margin-bottom:16px;">
|
|
|
|
|
|
<input type="datetime-local" x-model="scheduledAt"
|
|
|
|
|
|
style="width:100%; padding:10px; border:1px solid #fcd34d; border-radius:8px;
|
|
|
|
|
|
font-size:13px; box-sizing:border-box;">
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{{-- 费用摘要 --}}
|
|
|
|
|
|
<div x-show="selectedTier"
|
|
|
|
|
|
style="display:none; background:#fffbeb; border:1px solid #fde68a;
|
|
|
|
|
|
border-radius:10px; padding:12px 14px; font-size:12px; color:#92400e; margin-bottom:16px;">
|
|
|
|
|
|
<div style="display:flex; justify-content:space-between; margin-bottom:4px;">
|
|
|
|
|
|
<span>红包总额</span>
|
|
|
|
|
|
<strong x-text="selectedTier ? selectedTier.amount.toLocaleString() + ' 金' : '—'"></strong>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div style="display:flex; justify-content:space-between; margin-bottom:4px;">
|
|
|
|
|
|
<span>你需支付</span>
|
|
|
|
|
|
<strong x-text="myCost + ' 金'"></strong>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div x-show="payBy === 'split'" style="display:none; font-size:11px; color:#b45309;">
|
|
|
|
|
|
另一半将在婚礼触发后自动从另一方账号扣除
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<button x-on:click="doSetup()" :disabled="sending || !selectedTier"
|
|
|
|
|
|
style="width:100%; padding:13px; border:none; border-radius:12px; font-size:14px;
|
|
|
|
|
|
font-weight:bold; cursor:pointer; transition:all .2s;"
|
|
|
|
|
|
:style="(sending || !selectedTier) ?
|
|
|
|
|
|
'background:#f1f5f9; color:#94a3b8; cursor:not-allowed;' :
|
|
|
|
|
|
'background:linear-gradient(135deg,#f59e0b,#d97706); color:#fff;'">
|
|
|
|
|
|
<span x-text="sending ? '设置中…' : '🎊 确认举办婚礼'"></span>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{{-- ═══════════ 5. 婚礼红包弹窗(全局触发) ═══════════ --}}
|
2026-03-01 17:17:14 +08:00
|
|
|
|
<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);
|
2026-03-01 15:31:07 +08:00
|
|
|
|
z-index:9910; display:flex; align-items:center; justify-content:center;">
|
|
|
|
|
|
<div
|
|
|
|
|
|
style="width:380px; max-width:95vw; background:linear-gradient(160deg,#7c2d12,#9a3412);
|
|
|
|
|
|
border-radius:20px; box-shadow:0 24px 80px rgba(220,38,38,.5); overflow:hidden; text-align:center;
|
|
|
|
|
|
border:1px solid rgba(251,146,60,.3);">
|
|
|
|
|
|
<div style="padding:24px 20px 16px;">
|
|
|
|
|
|
<div style="font-size:52px; margin-bottom:8px; animation:pulse 1.5s infinite;">🧧</div>
|
|
|
|
|
|
<div style="color:#fef3c7; font-weight:bold; font-size:18px; margin-bottom:4px;" x-text="title"></div>
|
|
|
|
|
|
<div style="color:rgba(254,243,199,.7); font-size:12px; margin-bottom:16px;" x-text="subTitle"></div>
|
|
|
|
|
|
|
|
|
|
|
|
{{-- 未领取 --}}
|
|
|
|
|
|
<div x-show="!claimed" style="display:none;">
|
|
|
|
|
|
<div style="color:rgba(254,243,199,.5); font-size:11px; margin-bottom:12px;">
|
|
|
|
|
|
红包有效期 <strong style="color:#fcd34d;">24小时</strong>,过期自动消失
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<button x-on:click="doClaim()" :disabled="claiming"
|
|
|
|
|
|
style="padding:14px 40px; border:none; border-radius:50px; font-size:16px;
|
|
|
|
|
|
font-weight:bold; cursor:pointer; transition:all .2s;"
|
|
|
|
|
|
:style="claiming
|
|
|
|
|
|
?
|
|
|
|
|
|
'background:#fcd34d; color:#92400e; opacity:.7; cursor:not-allowed;' :
|
|
|
|
|
|
'background:linear-gradient(135deg,#fcd34d,#f59e0b); color:#92400e;'">
|
|
|
|
|
|
<span x-text="claiming ? '领取中…' : '🎁 点击领取'"></span>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{{-- 已领取 --}}
|
|
|
|
|
|
<div x-show="claimed" style="display:none;">
|
|
|
|
|
|
<div style="font-size:36px; margin-bottom:6px; color:#fcd34d; font-weight:bold;"
|
|
|
|
|
|
x-text="'+' + claimedAmount.toLocaleString() + ' 金'"></div>
|
|
|
|
|
|
<div style="color:#fef3c7; font-size:13px;">🎉 恭喜你领取了红包!</div>
|
|
|
|
|
|
<div style="color:rgba(254,243,199,.6); font-size:11px; margin-top:4px;">金币已自动到账</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div style="padding:0 20px 20px;">
|
|
|
|
|
|
<button x-on:click="close()"
|
|
|
|
|
|
style="padding:10px 32px; background:rgba(0,0,0,.3); border:none; border-radius:30px;
|
|
|
|
|
|
font-size:12px; color:rgba(254,243,199,.7); cursor:pointer;">
|
|
|
|
|
|
<span x-text="claimed ? '收下啦 ✨' : '关闭'"></span>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<style>
|
|
|
|
|
|
@keyframes pulse {
|
|
|
|
|
|
|
|
|
|
|
|
0%,
|
|
|
|
|
|
100% {
|
|
|
|
|
|
transform: scale(1);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
50% {
|
|
|
|
|
|
transform: scale(1.08);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
</style>
|
|
|
|
|
|
|
|
|
|
|
|
{{-- ═══════════ Alpine.js 组件脚本 ═══════════ --}}
|
|
|
|
|
|
<script>
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 求婚弹窗组件
|
|
|
|
|
|
*/
|
|
|
|
|
|
function marriageProposeModal() {
|
|
|
|
|
|
return {
|
|
|
|
|
|
show: false,
|
|
|
|
|
|
targetUsername: '',
|
|
|
|
|
|
marriageId: null, // 当前对方婚姻/求婚记录 ID(accept/reject 用)
|
|
|
|
|
|
rings: [],
|
|
|
|
|
|
selectedRing: null,
|
|
|
|
|
|
loading: false,
|
|
|
|
|
|
sending: false,
|
|
|
|
|
|
error: '',
|
|
|
|
|
|
|
|
|
|
|
|
async open(username) {
|
|
|
|
|
|
this.targetUsername = username;
|
|
|
|
|
|
this.selectedRing = null;
|
|
|
|
|
|
this.error = '';
|
|
|
|
|
|
this.loading = true;
|
|
|
|
|
|
this.show = true;
|
|
|
|
|
|
try {
|
|
|
|
|
|
const res = await fetch(window.chatContext.marriage.myRingsUrl, {
|
|
|
|
|
|
headers: {
|
|
|
|
|
|
'Accept': 'application/json'
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
const data = await res.json();
|
|
|
|
|
|
if (data.status === 'success') {
|
|
|
|
|
|
this.rings = data.rings;
|
|
|
|
|
|
if (this.rings.length > 0) this.selectedRing = this.rings[0].purchase_id;
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
this.rings = [];
|
|
|
|
|
|
}
|
|
|
|
|
|
this.loading = false;
|
|
|
|
|
|
},
|
|
|
|
|
|
|
2026-03-01 15:38:52 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 由 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;
|
|
|
|
|
|
},
|
|
|
|
|
|
|
2026-03-01 15:31:07 +08:00
|
|
|
|
close() {
|
|
|
|
|
|
this.show = false;
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
async doPropose() {
|
|
|
|
|
|
if (this.sending || !this.selectedRing) return;
|
2026-03-01 17:19:27 +08:00
|
|
|
|
|
2026-03-01 17:22:57 +08:00
|
|
|
|
// 费用信息已在弹窗内展示,此处无需二次确认弹窗
|
2026-03-01 17:19:27 +08:00
|
|
|
|
|
2026-03-01 15:31:07 +08:00
|
|
|
|
this.sending = true;
|
|
|
|
|
|
this.error = '';
|
|
|
|
|
|
try {
|
|
|
|
|
|
const res = await fetch(window.chatContext.marriage.proposeUrl, {
|
|
|
|
|
|
method: 'POST',
|
|
|
|
|
|
headers: {
|
|
|
|
|
|
'Content-Type': 'application/json',
|
|
|
|
|
|
'Accept': 'application/json',
|
|
|
|
|
|
'X-CSRF-TOKEN': document.querySelector('meta[name=csrf-token]').content
|
|
|
|
|
|
},
|
|
|
|
|
|
body: JSON.stringify({
|
|
|
|
|
|
target_username: this.targetUsername,
|
|
|
|
|
|
ring_purchase_id: this.selectedRing,
|
|
|
|
|
|
room_id: window.chatContext.roomId
|
|
|
|
|
|
})
|
|
|
|
|
|
});
|
|
|
|
|
|
const data = await res.json();
|
|
|
|
|
|
if (data.status === 'success') {
|
|
|
|
|
|
this.close();
|
|
|
|
|
|
window.chatDialog?.alert('💍 求婚成功!等待对方回应(有效期 48 小时)', '已发出', '#f43f5e');
|
|
|
|
|
|
} else {
|
|
|
|
|
|
this.error = data.message || '求婚失败';
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
this.error = '网络异常,请稍后重试';
|
|
|
|
|
|
}
|
|
|
|
|
|
this.sending = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 收到求婚弹窗组件
|
|
|
|
|
|
*/
|
|
|
|
|
|
function marriageIncomingModal() {
|
|
|
|
|
|
return {
|
|
|
|
|
|
show: false,
|
|
|
|
|
|
proposerName: '',
|
|
|
|
|
|
marriageId: null,
|
|
|
|
|
|
ringName: '',
|
|
|
|
|
|
ringIcon: '💍',
|
|
|
|
|
|
expiresAt: '',
|
|
|
|
|
|
acting: false,
|
|
|
|
|
|
|
|
|
|
|
|
open(detail) {
|
|
|
|
|
|
this.proposerName = detail.proposer_name || detail.proposer?.username || '';
|
|
|
|
|
|
this.marriageId = detail.marriage_id;
|
|
|
|
|
|
this.ringName = detail.ring_name || '';
|
|
|
|
|
|
this.ringIcon = detail.ring_icon || '💍';
|
|
|
|
|
|
this.expiresAt = detail.expires_at || '';
|
|
|
|
|
|
this.show = true;
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
close() {
|
|
|
|
|
|
this.show = false;
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
async doAccept() {
|
|
|
|
|
|
if (this.acting) return;
|
|
|
|
|
|
this.acting = true;
|
|
|
|
|
|
try {
|
|
|
|
|
|
const res = await fetch(window.chatContext.marriage.acceptUrl(this.marriageId), {
|
|
|
|
|
|
method: 'POST',
|
|
|
|
|
|
headers: {
|
|
|
|
|
|
'Content-Type': 'application/json',
|
|
|
|
|
|
'Accept': 'application/json',
|
|
|
|
|
|
'X-CSRF-TOKEN': document.querySelector('meta[name=csrf-token]').content
|
|
|
|
|
|
},
|
|
|
|
|
|
body: JSON.stringify({
|
|
|
|
|
|
room_id: window.chatContext.roomId
|
|
|
|
|
|
})
|
|
|
|
|
|
});
|
|
|
|
|
|
const data = await res.json();
|
|
|
|
|
|
this.close();
|
|
|
|
|
|
if (data.status !== 'success') {
|
|
|
|
|
|
window.chatDialog?.alert(data.message || '操作失败', '提示', '#cc4444');
|
|
|
|
|
|
}
|
|
|
|
|
|
// 结婚成功后会收到 MarriageAccepted 全局广播
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
window.chatDialog?.alert('网络异常', '错误', '#cc4444');
|
|
|
|
|
|
}
|
|
|
|
|
|
this.acting = false;
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
async doReject() {
|
|
|
|
|
|
if (this.acting) return;
|
|
|
|
|
|
this.acting = true;
|
|
|
|
|
|
try {
|
|
|
|
|
|
const res = await fetch(window.chatContext.marriage.rejectUrl(this.marriageId), {
|
|
|
|
|
|
method: 'POST',
|
|
|
|
|
|
headers: {
|
|
|
|
|
|
'Content-Type': 'application/json',
|
|
|
|
|
|
'Accept': 'application/json',
|
|
|
|
|
|
'X-CSRF-TOKEN': document.querySelector('meta[name=csrf-token]').content
|
|
|
|
|
|
},
|
|
|
|
|
|
body: JSON.stringify({
|
|
|
|
|
|
room_id: window.chatContext.roomId
|
|
|
|
|
|
})
|
|
|
|
|
|
});
|
|
|
|
|
|
const data = await res.json();
|
|
|
|
|
|
this.close();
|
|
|
|
|
|
if (data.status !== 'success') {
|
|
|
|
|
|
window.chatDialog?.alert(data.message || '操作失败', '提示', '#cc4444');
|
|
|
|
|
|
} else {
|
|
|
|
|
|
window.chatDialog?.alert('已婉拒对方的求婚', '操作完成', '#6b7280');
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
window.chatDialog?.alert('网络异常', '错误', '#cc4444');
|
|
|
|
|
|
}
|
|
|
|
|
|
this.acting = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 结婚成功弹窗组件(收到全局广播后触发)
|
|
|
|
|
|
*/
|
|
|
|
|
|
function marriageAcceptedModal() {
|
|
|
|
|
|
return {
|
|
|
|
|
|
show: false,
|
|
|
|
|
|
announcement: '',
|
|
|
|
|
|
subText: '',
|
|
|
|
|
|
marriageId: null,
|
|
|
|
|
|
isNewlywed: false,
|
|
|
|
|
|
|
|
|
|
|
|
open(detail) {
|
|
|
|
|
|
this.marriageId = detail.marriage_id;
|
|
|
|
|
|
this.announcement = `${detail.groom_name} 与 ${detail.bride_name} 喜结连理!`;
|
|
|
|
|
|
this.subText = detail.message || '愿百年好合,白头偕老!';
|
|
|
|
|
|
// 仅当前用户是新婚双方之一时显示举办婚礼按钮
|
|
|
|
|
|
const me = window.chatContext.username;
|
|
|
|
|
|
this.isNewlywed = (detail.groom_name === me || detail.bride_name === me);
|
|
|
|
|
|
this.show = true;
|
|
|
|
|
|
// 播放烟花特效
|
|
|
|
|
|
if (window.EffectManager) {
|
|
|
|
|
|
window.EffectManager.play('fireworks');
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
close() {
|
|
|
|
|
|
this.show = false;
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
openWeddingSetup() {
|
|
|
|
|
|
openWeddingSetupModal(this.marriageId);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 婚礼设置弹窗组件
|
|
|
|
|
|
*/
|
|
|
|
|
|
function weddingSetupModal() {
|
|
|
|
|
|
return {
|
|
|
|
|
|
show: false,
|
|
|
|
|
|
marriageId: null,
|
|
|
|
|
|
tiers: [],
|
|
|
|
|
|
selectedTier: null,
|
|
|
|
|
|
payBy: 'groom',
|
|
|
|
|
|
timing: 'now',
|
|
|
|
|
|
scheduledAt: '',
|
|
|
|
|
|
loading: false,
|
|
|
|
|
|
sending: false,
|
|
|
|
|
|
error: '',
|
|
|
|
|
|
|
|
|
|
|
|
get myCost() {
|
|
|
|
|
|
if (!this.selectedTier) return 0;
|
|
|
|
|
|
return this.payBy === 'split' ?
|
|
|
|
|
|
Math.ceil(this.selectedTier.amount / 2) :
|
|
|
|
|
|
this.selectedTier.amount;
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
async open(marriageId) {
|
|
|
|
|
|
this.marriageId = marriageId;
|
|
|
|
|
|
this.selectedTier = null;
|
|
|
|
|
|
this.payBy = 'groom';
|
|
|
|
|
|
this.timing = 'now';
|
|
|
|
|
|
this.scheduledAt = '';
|
|
|
|
|
|
this.error = '';
|
|
|
|
|
|
this.loading = true;
|
|
|
|
|
|
this.show = true;
|
|
|
|
|
|
try {
|
|
|
|
|
|
const res = await fetch(window.chatContext.marriage.weddingTiersUrl, {
|
|
|
|
|
|
headers: {
|
|
|
|
|
|
'Accept': 'application/json'
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
const data = await res.json();
|
|
|
|
|
|
if (data.status === 'success') {
|
|
|
|
|
|
this.tiers = data.tiers;
|
|
|
|
|
|
if (this.tiers.length > 0) this.selectedTier = this.tiers[0];
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
this.tiers = [];
|
|
|
|
|
|
}
|
|
|
|
|
|
this.loading = false;
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
close() {
|
|
|
|
|
|
this.show = false;
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
async doSetup() {
|
|
|
|
|
|
if (this.sending || !this.selectedTier) return;
|
|
|
|
|
|
this.error = '';
|
|
|
|
|
|
this.sending = true;
|
|
|
|
|
|
try {
|
|
|
|
|
|
const body = {
|
|
|
|
|
|
tier_id: this.selectedTier.id,
|
|
|
|
|
|
pay_by: this.payBy,
|
|
|
|
|
|
ceremony_type: this.timing,
|
|
|
|
|
|
room_id: window.chatContext.roomId,
|
|
|
|
|
|
};
|
|
|
|
|
|
if (this.timing === 'scheduled') {
|
|
|
|
|
|
body.scheduled_at = this.scheduledAt;
|
|
|
|
|
|
}
|
|
|
|
|
|
const res = await fetch(window.chatContext.marriage.weddingSetupUrl(this.marriageId), {
|
|
|
|
|
|
method: 'POST',
|
|
|
|
|
|
headers: {
|
|
|
|
|
|
'Content-Type': 'application/json',
|
|
|
|
|
|
'Accept': 'application/json',
|
|
|
|
|
|
'X-CSRF-TOKEN': document.querySelector('meta[name=csrf-token]').content
|
|
|
|
|
|
},
|
|
|
|
|
|
body: JSON.stringify(body)
|
|
|
|
|
|
});
|
|
|
|
|
|
const data = await res.json();
|
|
|
|
|
|
if (data.status === 'success') {
|
|
|
|
|
|
this.close();
|
|
|
|
|
|
window.chatDialog?.alert(
|
|
|
|
|
|
this.timing === 'now' ?
|
|
|
|
|
|
'🎊 婚礼已开始!红包正在分发给在线用户…' :
|
|
|
|
|
|
'⏰ 婚礼已预约,时间到时将自动举办!',
|
|
|
|
|
|
'设置成功', '#f59e0b'
|
|
|
|
|
|
);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
this.error = data.message || '设置失败';
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
this.error = '网络异常,请稍后重试';
|
|
|
|
|
|
}
|
|
|
|
|
|
this.sending = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 婚礼红包弹窗组件
|
|
|
|
|
|
*/
|
|
|
|
|
|
function weddingEnvelopeModal() {
|
|
|
|
|
|
return {
|
|
|
|
|
|
show: false,
|
|
|
|
|
|
marriageId: null,
|
|
|
|
|
|
ceremonyId: null,
|
|
|
|
|
|
title: '',
|
|
|
|
|
|
subTitle: '',
|
|
|
|
|
|
claimed: false,
|
|
|
|
|
|
claiming: false,
|
|
|
|
|
|
claimedAmount: 0,
|
|
|
|
|
|
|
|
|
|
|
|
open(detail) {
|
|
|
|
|
|
this.marriageId = detail.marriage_id;
|
|
|
|
|
|
this.ceremonyId = detail.ceremony_id;
|
|
|
|
|
|
this.title = `${detail.groom_name} × ${detail.bride_name} 婚礼红包`;
|
|
|
|
|
|
this.subTitle = detail.tier_name ? `【${detail.tier_name}】普天同庆` : '婚礼庆典红包';
|
|
|
|
|
|
this.claimed = false;
|
|
|
|
|
|
this.claimedAmount = 0;
|
|
|
|
|
|
this.show = true;
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
close() {
|
|
|
|
|
|
this.show = false;
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
async doClaim() {
|
|
|
|
|
|
if (this.claiming || this.claimed) return;
|
|
|
|
|
|
this.claiming = true;
|
|
|
|
|
|
try {
|
|
|
|
|
|
const res = await fetch(`/wedding/${this.marriageId}/claim`, {
|
|
|
|
|
|
method: 'POST',
|
|
|
|
|
|
headers: {
|
|
|
|
|
|
'Content-Type': 'application/json',
|
|
|
|
|
|
'Accept': 'application/json',
|
|
|
|
|
|
'X-CSRF-TOKEN': document.querySelector('meta[name=csrf-token]').content
|
|
|
|
|
|
},
|
|
|
|
|
|
body: JSON.stringify({
|
|
|
|
|
|
ceremony_id: this.ceremonyId
|
|
|
|
|
|
})
|
|
|
|
|
|
});
|
|
|
|
|
|
const data = await res.json();
|
|
|
|
|
|
if (data.status === 'success') {
|
|
|
|
|
|
this.claimed = true;
|
|
|
|
|
|
this.claimedAmount = data.amount || 0;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
window.chatDialog?.alert(data.message || '领取失败', '提示', '#f59e0b');
|
|
|
|
|
|
if (data.message?.includes('已领取') || data.message?.includes('已过期')) {
|
|
|
|
|
|
this.claimed = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
window.chatDialog?.alert('网络异常', '错误', '#cc4444');
|
|
|
|
|
|
}
|
|
|
|
|
|
this.claiming = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ───────── 全局入口函数 ─────────────────────────────────
|
|
|
|
|
|
|
2026-03-01 15:38:52 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 打开求婚弹窗(从名片按钮调用)。
|
|
|
|
|
|
* 先检查背包是否有戒指:
|
|
|
|
|
|
* - 有 → 直接开弹窗(戒指列表已预加载,无需二次请求)
|
|
|
|
|
|
* - 无 → 提示用户前往商店购买
|
|
|
|
|
|
*/
|
|
|
|
|
|
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) {
|
2026-03-01 15:47:01 +08:00
|
|
|
|
// 直接打开当前页面的商店浮窗(toolbar 中的 openShopModal)
|
|
|
|
|
|
if (typeof window.openShopModal === 'function') {
|
|
|
|
|
|
window.openShopModal();
|
|
|
|
|
|
}
|
2026-03-01 15:38:52 +08:00
|
|
|
|
}
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 有戒指:打开弹窗,将已加载的列表传入(避免二次请求)
|
2026-03-01 15:31:07 +08:00
|
|
|
|
const el = document.getElementById('marriage-propose-modal');
|
2026-03-01 15:38:52 +08:00
|
|
|
|
if (el) Alpine.$data(el).openWithRings(username, rings);
|
2026-03-01 15:31:07 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** 打开婚礼设置弹窗 */
|
|
|
|
|
|
function openWeddingSetupModal(marriageId) {
|
|
|
|
|
|
const el = document.getElementById('wedding-setup-modal');
|
|
|
|
|
|
if (el) Alpine.$data(el).open(marriageId);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ───────── WebSocket 事件处理 ───────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
/** 收到全局结婚公告 */
|
|
|
|
|
|
window.addEventListener('chat:marriage-accepted', (e) => {
|
|
|
|
|
|
const detail = e.detail;
|
|
|
|
|
|
const el = document.getElementById('marriage-accepted-modal');
|
|
|
|
|
|
if (el) Alpine.$data(el).open(detail);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
/** 收到求婚通知(私人频道,目标方) */
|
|
|
|
|
|
window.addEventListener('chat:marriage-proposed', (e) => {
|
|
|
|
|
|
const detail = e.detail;
|
|
|
|
|
|
const el = document.getElementById('marriage-incoming-modal');
|
|
|
|
|
|
if (el) Alpine.$data(el).open(detail);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
/** 收到婚礼庆典(全局,显示红包) */
|
|
|
|
|
|
window.addEventListener('chat:wedding-celebration', (e) => {
|
|
|
|
|
|
const detail = e.detail;
|
|
|
|
|
|
const el = document.getElementById('wedding-envelope-modal');
|
|
|
|
|
|
if (el) Alpine.$data(el).open(detail);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
/** 求婚被拒(私人频道,发起方) */
|
|
|
|
|
|
window.addEventListener('chat:marriage-rejected', (e) => {
|
|
|
|
|
|
const {
|
|
|
|
|
|
proposer_name,
|
|
|
|
|
|
partner_name
|
|
|
|
|
|
} = e.detail;
|
|
|
|
|
|
window.chatDialog?.alert(
|
|
|
|
|
|
`${partner_name} 婉拒了你的求婚,戒指随之遗失… 💔`,
|
|
|
|
|
|
'求婚被拒绝',
|
|
|
|
|
|
'#6b7280'
|
|
|
|
|
|
);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
/** 求婚超时(私人频道,发起方) */
|
|
|
|
|
|
window.addEventListener('chat:marriage-expired', (e) => {
|
|
|
|
|
|
window.chatDialog?.alert(
|
|
|
|
|
|
'你的求婚超时未获回应,戒指已消失… ⏰',
|
|
|
|
|
|
'求婚已过期',
|
|
|
|
|
|
'#9ca3af'
|
|
|
|
|
|
);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
/** 接到协议离婚申请(私人频道,对方) */
|
|
|
|
|
|
window.addEventListener('chat:divorce-requested', (e) => {
|
|
|
|
|
|
const {
|
|
|
|
|
|
initiator_name,
|
|
|
|
|
|
marriage_id
|
|
|
|
|
|
} = e.detail;
|
|
|
|
|
|
window.chatDialog?.confirm(
|
|
|
|
|
|
`${initiator_name} 申请与你协议离婚,是否同意?`,
|
|
|
|
|
|
'离婚申请 💔'
|
|
|
|
|
|
).then(ok => {
|
|
|
|
|
|
if (!ok) return;
|
|
|
|
|
|
fetch(window.chatContext.marriage.confirmDivorceUrl(marriage_id), {
|
|
|
|
|
|
method: 'POST',
|
|
|
|
|
|
headers: {
|
|
|
|
|
|
'Content-Type': 'application/json',
|
|
|
|
|
|
'Accept': 'application/json',
|
|
|
|
|
|
'X-CSRF-TOKEN': document.querySelector('meta[name=csrf-token]').content
|
|
|
|
|
|
},
|
|
|
|
|
|
body: JSON.stringify({
|
|
|
|
|
|
room_id: window.chatContext.roomId
|
|
|
|
|
|
})
|
|
|
|
|
|
}).then(r => r.json()).then(data => {
|
|
|
|
|
|
window.chatDialog?.alert(data.message, data.status === 'success' ? '操作完成' :
|
|
|
|
|
|
'失败', data.status === 'success' ? '#6b7280' : '#cc4444');
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
/** 红包领取成功通知(私人频道) */
|
|
|
|
|
|
window.addEventListener('chat:envelope-claimed', (e) => {
|
|
|
|
|
|
const {
|
|
|
|
|
|
amount
|
|
|
|
|
|
} = e.detail;
|
|
|
|
|
|
window.chatDialog?.alert(`+${amount.toLocaleString()} 金币已到账 🎉`, '红包到手!', '#f59e0b');
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
/** 结婚/离婚全局公告:在聊天消息区追加一条系统消息 */
|
|
|
|
|
|
window.addEventListener('chat:marriage-accepted', (e) => {
|
|
|
|
|
|
const {
|
|
|
|
|
|
groom_name,
|
|
|
|
|
|
bride_name,
|
|
|
|
|
|
message
|
|
|
|
|
|
} = e.detail;
|
|
|
|
|
|
if (typeof appendSystemMessage === 'function') {
|
|
|
|
|
|
appendSystemMessage(`💑 ${groom_name} 与 ${bride_name} 喜结连理!${message || ''}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
window.addEventListener('chat:marriage-divorced', (e) => {
|
|
|
|
|
|
const {
|
|
|
|
|
|
user_name,
|
|
|
|
|
|
partner_name
|
|
|
|
|
|
} = e.detail;
|
|
|
|
|
|
if (typeof appendSystemMessage === 'function') {
|
|
|
|
|
|
appendSystemMessage(`💔 ${user_name} 与 ${partner_name} 解除了婚姻关系。`);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
/** 页面加载完成后初始化私人频道监听 */
|
|
|
|
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
|
|
|
|
const userId = window.chatContext?.userId;
|
|
|
|
|
|
if (userId && typeof window.initMarriagePrivateChannel === 'function') {
|
|
|
|
|
|
// 延迟初始化,确保 Echo 已就绪
|
|
|
|
|
|
setTimeout(() => window.initMarriagePrivateChannel(userId), 1500);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
</script>
|