feat: 神秘箱子系统完整实现 + 婚姻状态弹窗 + 工具栏优化

## 新功能
- 神秘箱子系统(MysteryBox)完整实现:
  - 新增 MysteryBox / MysteryBoxClaim 模型及迁移文件
  - DropMysteryBoxJob / ExpireMysteryBoxJob 队列作业
  - MysteryBoxController(/mystery-box/status + /mystery-box/claim)
  - 支持三种类型:普通箱(500~2000金)/ 稀有箱(5000~20000金)/ 黑化箱(陷阱扣200~1000金)
  - 调度器自动投放 + 管理员手动投放
  - CurrencySource 新增 MYSTERY_BOX / MYSTERY_BOX_TRAP 枚举

- 婚姻状态弹窗(工具栏「婚姻」按钮):
  - 工具栏「呼叫」改为「婚姻」,点击打开婚姻状态弹窗
  - 动态渲染三种状态:单身 / 求婚中 / 已婚
  - 被求婚方可直接「答应 / 婉拒」;已婚可申请离婚(含二次确认)

## 优化修复
- frame.blade.php:Alpine.js CDN 补加 defer,修复所有组件初始化报错
- scripts.blade.php:神秘箱子暗号主动拦截(不依赖轮询),领取成功后弹 chatDialog 展示结果,更新金币余额
- MysteryBoxController:claim() 时 change() 补传 room_id 记录来源房间
- 后台游戏管理页(game-configs):投放箱子按钮颜色修复;弹窗替换为 window.adminDialog
- admin/layouts:新增全局 adminDialog 弹窗组件(替代原生 alert/confirm)
- baccarat-panel:FAB 拖动重构为 Alpine.js baccaratFab() 组件,与 slotFab 一致
- GAMES_TODO.md:神秘箱子移入已完成区,补全修复记录
This commit is contained in:
2026-03-03 19:29:43 +08:00
parent 40fcce2db3
commit 602dcd7cf1
21 changed files with 1799 additions and 139 deletions
+278 -1
View File
@@ -19,7 +19,7 @@
<div class="tool-btn" onclick="saveExp()" title="手动存经验点">存点</div>
<div class="tool-btn" onclick="alert('🚧 娱乐功能开发中,敬请期待!')" title="娱乐(待开发)">娱乐</div>
<div class="tool-btn" onclick="alert('🚧 银行功能开发中,敬请期待!')" title="银行(待开发)">银行</div>
<div class="tool-btn" onclick="alert('🚧 呼叫功能开发中,敬请期待!')" title="呼叫(待开发)">呼叫</div>
<div class="tool-btn" onclick="openMarriageStatusModal()" title="婚姻状态">婚姻</div>
<div class="tool-btn" onclick="openFriendPanel()" title="好友列表">好友</div>
<div class="tool-btn" onclick="openAvatarPicker()" title="修改头像">头像</div>
<div class="tool-btn" onclick="document.getElementById('settings-modal').style.display='flex'" title="个人设置">设置
@@ -1051,3 +1051,280 @@
}
})();
</script>
{{-- ═══════════ 婚姻状态弹窗 ═══════════ --}}
<div id="marriage-status-modal"
style="display:none; position:fixed; inset:0; background:rgba(0,0,0,.5);
z-index:9999; justify-content:center; align-items:center;">
<div
style="background:#fff; border-radius:10px; width:360px; max-width:94vw;
box-shadow:0 12px 40px rgba(0,0,0,.3); overflow:hidden;
animation:gdSlideIn .18s ease; display:flex; flex-direction:column;">
{{-- 标题栏 --}}
<div
style="background:linear-gradient(135deg,#be185d,#f43f5e,#ec4899);
color:#fff; padding:12px 16px;
display:flex; align-items:center; justify-content:space-between;">
<span style="font-size:14px; font-weight:bold;">💍 我的婚姻</span>
<span onclick="closeMarriageStatusModal()"
style="cursor:pointer; font-size:18px; opacity:.8; line-height:1;"></span>
</div>
{{-- 内容区(动态渲染) --}}
<div id="marriage-status-body" style="padding:16px; min-height:120px;">
<div style="text-align:center; color:#aaa; padding:30px 0; font-size:12px;">加载中…</div>
</div>
{{-- 底部操作区 --}}
<div id="marriage-status-footer" style="padding:0 16px 16px; display:flex; gap:8px;"></div>
</div>
</div>
<script>
/**
* 婚姻状态弹窗——工具栏点击「婚姻」按钮触发。
* 调用 /marriage/status 接口,展示当前用户婚姻状态(单身/求婚中/已婚)。
*/
(function() {
const CSRF = () => document.querySelector('meta[name="csrf-token"]')?.content ?? '';
const $ = id => document.getElementById(id);
/** 打开弹窗并拉取状态 */
window.openMarriageStatusModal = function() {
$('marriage-status-modal').style.display = 'flex';
$('marriage-status-body').innerHTML =
'<div style="text-align:center;color:#aaa;padding:30px 0;font-size:12px;">加载中…</div>';
$('marriage-status-footer').innerHTML = '';
fetch('/marriage/status', {
headers: {
Accept: 'application/json'
}
})
.then(r => r.json())
.then(renderMarriageStatus)
.catch(() => {
$('marriage-status-body').innerHTML =
'<div style="text-align:center;color:#e55;padding:30px 0;font-size:12px;">❌ 加载失败,请稍后重试</div>';
});
};
/** 关闭弹窗 */
window.closeMarriageStatusModal = function() {
$('marriage-status-modal').style.display = 'none';
};
// 点击遮罩关闭
$('marriage-status-modal').addEventListener('click', function(e) {
if (e.target === this) closeMarriageStatusModal();
});
/**
* 根据接口返回数据渲染弹窗内容。
*
* @param {object} data `/marriage/status` 响应 JSON
*/
function renderMarriageStatus(data) {
const body = $('marriage-status-body');
const footer = $('marriage-status-footer');
// ── 单身 ────────────────────────────────────
if (!data.status || data.status === 'none' || !data.marriage) {
body.innerHTML = `
<div style="text-align:center; padding:16px 0;">
<div style="font-size:40px; margin-bottom:10px;">🕊️</div>
<div style="font-size:14px; font-weight:bold; color:#555;">目前单身</div>
<div style="font-size:11px; color:#999; margin-top:6px; line-height:1.7;">
还没有婚姻记录。<br>可在用户名片上点击「求婚」发起求婚。
</div>
</div>`;
footer.innerHTML = `
<button onclick="closeMarriageStatusModal()"
style="flex:1; padding:9px; background:#f3f4f6; color:#555;
border:1px solid #d1d5db; border-radius:6px; font-size:13px; cursor:pointer;">
关闭
</button>`;
return;
}
const m = data.marriage;
const isMine = (m.user && m.user.username === window.__chatUser?.username) ||
window.__chatUser?.id === m.user?.id ||
window.__chatUser?.id === m.partner?.id;
// 确定"另一方"信息(我可能是 user 也可能是 partner
const me = window.__chatUser;
const other = (m.user?.id === me?.id) ? m.partner : m.user;
const iAmUser = (m.user?.id === me?.id);
// ── 求婚中 ──────────────────────────────────
if (data.status === 'pending') {
const iProposed = iAmUser; // user_id 是发起方
const expireAt = m.expires_at ? new Date(m.expires_at).toLocaleString('zh-CN', {
hour12: false
}) : '—';
const ringHtml = m.ring ?
`<span style="font-size:13px;">${m.ring.icon ?? '💍'} ${m.ring.name}</span>` : '';
body.innerHTML = `
<div style="text-align:center; padding:8px 0;">
<div style="font-size:36px; margin-bottom:8px;">💌</div>
<div style="font-size:14px; font-weight:bold; color:#be185d;">
${iProposed ? '你向 ' + (other?.username ?? '—') + ' 发出了求婚' : (other?.username ?? '—') + ' 向你求婚啦!'}
</div>
${ringHtml ? `<div style="margin:8px 0; font-size:12px; color:#666;">戒指:${ringHtml}</div>` : ''}
<div style="font-size:11px; color:#999; margin-top:6px;">
过期时间:${expireAt}
</div>
</div>`;
if (!iProposed) {
// 被求婚方:可以接受 / 拒绝
footer.innerHTML = `
<button onclick="marriageAction('${m.id}','reject'); closeMarriageStatusModal();"
style="flex:1;padding:9px;background:#f3f4f6;color:#555;
border:1px solid #d1d5db;border-radius:6px;font-size:13px;cursor:pointer;">
😢 婉拒
</button>
<button onclick="marriageAction('${m.id}','accept'); closeMarriageStatusModal();"
style="flex:1;padding:9px;background:linear-gradient(135deg,#be185d,#f43f5e);
color:#fff;border:none;border-radius:6px;font-size:13px;
font-weight:bold;cursor:pointer;">
💑 答应啦!
</button>`;
} else {
footer.innerHTML = `
<button onclick="closeMarriageStatusModal()"
style="flex:1;padding:9px;background:#f3f4f6;color:#555;
border:1px solid #d1d5db;border-radius:6px;font-size:13px;cursor:pointer;">
关闭(等待对方回应)
</button>`;
}
return;
}
// ── 已婚 ────────────────────────────────────
if (data.status === 'married') {
const levelIcon = m.level_icon ?? '💑';
const levelName = m.level_name ?? '新婚';
const days = m.days ?? 0;
const intimacy = m.intimacy ?? 0;
const marriedAt = m.married_at ?? '—';
const ringHtml = m.ring ? `${m.ring.icon ?? '💍'} ${m.ring.name}` : '无';
body.innerHTML = `
<div style="text-align:center; margin-bottom:12px;">
<div style="font-size:36px; margin-bottom:6px;">${levelIcon}</div>
<div style="font-size:14px; font-weight:bold; color:#be185d;">
已与 <strong>${other?.username ?? '—'}</strong> 成婚 🎉
</div>
<div style="font-size:12px; color:#999; margin-top:4px;">婚姻等级:${levelName}</div>
</div>
<div style="display:grid; grid-template-columns:1fr 1fr; gap:8px; font-size:12px;">
<div style="background:#fdf2f8;border:1px solid #fbcfe8;border-radius:6px;padding:10px;text-align:center;">
<div style="color:#be185d;font-weight:bold;font-size:18px;">${days}</div>
<div style="color:#888;margin-top:2px;">携手天数</div>
</div>
<div style="background:#fdf4ff;border:1px solid #e9d5ff;border-radius:6px;padding:10px;text-align:center;">
<div style="color:#7c3aed;font-weight:bold;font-size:18px;">${Number(intimacy).toLocaleString()}</div>
<div style="color:#888;margin-top:2px;">亲密度</div>
</div>
<div style="background:#f0fdf4;border:1px solid #bbf7d0;border-radius:6px;padding:8px 10px;grid-column:1/-1;">
<span style="color:#666;">💍 戒指:</span><span style="color:#333;">${ringHtml}</span>
&nbsp;&nbsp;
<span style="color:#666;">📅 婚期:</span><span style="color:#333;">${marriedAt}</span>
</div>
</div>`;
// 已婚底部:离婚入口(需要二次确认)
footer.innerHTML = `
<button onclick="closeMarriageStatusModal()"
style="flex:1;padding:9px;background:#f3f4f6;color:#555;
border:1px solid #d1d5db;border-radius:6px;font-size:13px;cursor:pointer;">
关闭
</button>
<button onclick="tryDivorce('${m.id}')"
style="flex:.8;padding:9px;border:1px solid #fca5a5;background:#fff;
color:#dc2626;border-radius:6px;font-size:12px;cursor:pointer;">
💔 申请离婚
</button>`;
return;
}
// 其他状态(divorced 等)
body.innerHTML =
`<div style="text-align:center;color:#999;padding:30px 0;font-size:12px;">暂无有效婚姻记录</div>`;
footer.innerHTML = `
<button onclick="closeMarriageStatusModal()"
style="flex:1;padding:9px;background:#f3f4f6;color:#555;
border:1px solid #d1d5db;border-radius:6px;font-size:13px;cursor:pointer;">关闭</button>`;
}
/**
* 通用婚姻操作(接受 / 拒绝求婚)
*
* @param {string|number} marriageId marriage 记录 ID
* @param {string} action 'accept' | 'reject'
*/
window.marriageAction = async function(marriageId, action) {
try {
const res = await fetch(`/marriage/${marriageId}/${action}`, {
method: 'POST',
headers: {
'X-CSRF-TOKEN': CSRF(),
'Content-Type': 'application/json',
Accept: 'application/json',
},
});
const data = await res.json();
if (data.ok) {
window.chatDialog?.alert(data.message || (action === 'accept' ? '已接受求婚!' : '已婉拒求婚'),
action === 'accept' ? '💑 恭喜!' : '提示', action === 'accept' ? '#be185d' :
'#6b7280');
} else {
window.chatDialog?.alert(data.message || '操作失败', '提示', '#f59e0b');
}
} catch {
window.chatDialog?.alert('网络异常,请稍后重试', '错误', '#ef4444');
}
};
/**
* 申请离婚(先弹确认框,再调接口)
*
* @param {string|number} marriageId marriage 记录 ID
*/
window.tryDivorce = async function(marriageId) {
closeMarriageStatusModal();
const confirmed = await window.chatDialog?.confirm(
'申请协议离婚后,对方有权同意或拒绝(拒绝即转为强制离婚,双方均扣除魅力值)。\n\n确定要申请吗?',
'💔 申请离婚',
'#dc2626',
);
if (!confirmed) {
return;
}
try {
const res = await fetch(`/marriage/${marriageId}/divorce`, {
method: 'POST',
headers: {
'X-CSRF-TOKEN': CSRF(),
'Content-Type': 'application/json',
Accept: 'application/json',
},
body: JSON.stringify({
type: 'mutual'
}),
});
const data = await res.json();
window.chatDialog?.alert(data.message || '申请已发送', '提示', data.ok ? '#10b981' : '#f59e0b');
} catch {
window.chatDialog?.alert('网络异常,请稍后重试', '错误', '#ef4444');
}
};
})();
</script>