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