Files
chatroom/resources/views/chat/partials/toolbar.blade.php
lkddi f45483bcba 功能更新与UI优化:游戏图标移除、用户名片修复、婚礼红包界面重设计
- 移除聊天室右下角浮动游戏图标(占卜、百家乐、赛马、老虎机)
- 用户名片按钮区:修复已婚/已好友时按钮换行问题,统一单行显示
- 婚礼红包弹窗:重设计为喜庆鲜红背景,领取按钮改为圆形米黄样式
- 新增婚礼红包恢复接口(/wedding/pending-envelopes),刷新后自动恢复领取按钮
- 修复 Alpine :style 字符串覆盖静态 style 导致圆形按钮失效的问题
- 撤职后用户等级改为根据经验值重新计算,不再无条件重置为1
- 管理员修改用户经验值后自动重算等级,有职务用户等级锁定
- 娱乐大厅钓鱼游戏按钮直接调用 startFishing() 简化操作流程
- 新增赛马、占卜、百家乐游戏及相关后端逻辑
2026-03-03 23:19:59 +08:00

1331 lines
55 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{{--
文件功能:聊天室竖向工具条(中间导航栏)+ 关联弹窗
包含:
1. 工具条按钮(管理、商店、存点、头像、设置等)
2. 头像选择弹窗
3. 个人设置弹窗(密码、资料、密保)
4. 商店弹窗(从工具栏点击打开的全屏模态)
5. 对应的 JS 操作函数
依赖变量:$user, $superLevel, $room
@author ChatRoom Laravel
@version 1.0.0
--}}
{{-- ═══════════ 竖向工具条按钮 ═══════════ --}}
<div class="chat-toolbar" id="toolbar-strip">
<div class="tool-btn" onclick="openShopModal()" title="购买道具">商店</div>
<div class="tool-btn" onclick="saveExp()" title="手动存经验点">存点</div>
<div class="tool-btn" onclick="openGameHall()" 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="个人设置">设置
</div>
<div class="tool-btn" onclick="window.open('{{ route('feedback.index') }}', '_blank')" title="反馈">反馈</div>
<div class="tool-btn" onclick="window.open('{{ route('guestbook.index') }}', '_blank')" title="留言板/私信">留言</div>
<div class="tool-btn" onclick="window.open('{{ route('guide') }}', '_blank')" title="规则/帮助">规则</div>
@if ($user->id === 1 || $user->activePosition()->exists())
<div class="tool-btn" style="color: #ffcc00;" onclick="window.open('/admin', '_blank')" title="管理后台">管理</div>
<div class="tool-btn" onclick="window.open('{{ route('leaderboard.index') }}', '_blank')" title="排行榜">排行
</div>
@else
<div class="tool-btn" onclick="window.open('{{ route('leaderboard.index') }}', '_blank')" title="排行榜">排行
</div>
@endif
<div class="tool-btn" style="color: #ffaaaa;"
onclick="window.chatDialog.confirm('确定要离开聊天室吗?', '离开聊天室').then(ok => { if (ok) leaveRoom(); })" title="离开聊天室">
离开
</div>
</div>
{{-- ═══════════ 头像选择弹窗 ═══════════ --}}
<div id="avatar-picker-modal"
style="display:none; position:fixed; top:0; left:0; right:0; bottom:0;
background:rgba(0,0,0,0.5); z-index:9999; justify-content:center; align-items:center;">
<div
style="background:#fff; width:600px; max-height:80vh; border-radius:6px; overflow:hidden;
box-shadow:0 4px 20px rgba(0,0,0,0.3); display:flex; flex-direction:column;">
{{-- 标题栏 --}}
<div
style="background:#336699; color:#fff; padding:10px 16px; font-size:14px; font-weight:bold;
display:flex; justify-content:space-between; align-items:center;">
<span>🖼 修改头像(原版风格)</span>
<span style="cursor:pointer; font-size:18px;" onclick="closeAvatarPicker()"></span>
</div>
{{-- 预览区 --}}
<div
style="padding:10px 16px; background:#f0f6ff; border-bottom:1px solid #ddd; display:flex; align-items:center; gap:12px;">
<span style="font-size:12px; color:#666;">当前选中:</span>
<img id="avatar-preview" src="/images/headface/{{ $user->usersf ?: '1.gif' }}"
style="width:40px; height:40px; border:2px solid #336699; border-radius:4px;">
<span id="avatar-selected-name" style="font-size:12px; color:#333;">{{ $user->usersf ?: '未设置' }}</span>
<button id="avatar-save-btn" disabled onclick="saveAvatar()"
style="margin-left:auto; padding:5px 16px; background:#336699; color:#fff; border:none;
border-radius:3px; font-size:12px; cursor:pointer;">确定更换</button>
</div>
{{-- 头像网格 --}}
<div id="avatar-grid"
style="flex:1; overflow-y:auto; padding:10px; display:flex; flex-wrap:wrap;
gap:4px; align-content:flex-start;">
</div>
</div>
</div>
{{-- ═══════════ 个人设置弹窗 ═══════════ --}}
<div id="settings-modal"
style="display:none; position:fixed; top:0; left:0; right:0; bottom:0;
background:rgba(0,0,0,0.5); z-index:9999; justify-content:center; align-items:center;">
<div
style="background:#fff; border-radius:8px; width:380px; max-height:90vh; overflow-y:auto;
box-shadow:0 8px 32px rgba(0,0,0,0.3);">
{{-- 标题栏 --}}
<div
style="background:linear-gradient(135deg,#336699,#5a8fc0); color:#fff;
padding:12px 16px; border-radius:8px 8px 0 0; display:flex; justify-content:space-between; align-items:center;">
<span style="font-size:14px; font-weight:bold;">⚙️ 个人设置</span>
<span onclick="document.getElementById('settings-modal').style.display='none'"
style="cursor:pointer; font-size:18px; opacity:0.8;">&times;</span>
</div>
<div style="padding:16px;">
{{-- 修改密码 --}}
<div style="margin-bottom:16px; border:1px solid #e0e0e0; border-radius:6px; padding:12px;">
<div style="font-size:12px; font-weight:bold; color:#336699; margin-bottom:8px;">🔒 修改密码</div>
<div style="display:flex; flex-direction:column; gap:6px;">
<input id="set-old-pwd" type="password" placeholder="当前旧密码"
style="padding:6px 8px; border:1px solid #ccc; border-radius:4px; font-size:12px;">
<input id="set-new-pwd" type="password" placeholder="新密码至少6位"
style="padding:6px 8px; border:1px solid #ccc; border-radius:4px; font-size:12px;">
<input id="set-new-pwd2" type="password" placeholder="确认新密码"
style="padding:6px 8px; border:1px solid #ccc; border-radius:4px; font-size:12px;">
<button onclick="savePassword()"
style="padding:6px; background:#336699; color:#fff; border:none; border-radius:4px;
font-size:12px; cursor:pointer;">确定修改密码</button>
</div>
</div>
{{-- 个人资料 --}}
<div style="margin-bottom:16px; border:1px solid #e0e0e0; border-radius:6px; padding:12px;">
<div style="font-size:12px; font-weight:bold; color:#336699; margin-bottom:8px;">👤 个人资料</div>
<div style="display:flex; flex-direction:column; gap:6px;">
<div style="display:flex; align-items:center; gap:8px;">
<label style="font-size:12px; width:50px; text-align:right;">性别:</label>
<select id="set-sex"
style="flex:1; padding:5px; border:1px solid #ccc; border-radius:4px; font-size:12px;">
<option value="1" {{ Auth::user()->sex == 1 ? 'selected' : '' }}></option>
<option value="2" {{ Auth::user()->sex == 2 ? 'selected' : '' }}></option>
<option value="0" {{ Auth::user()->sex == 0 ? 'selected' : '' }}>保密</option>
</select>
</div>
<div style="display:flex; align-items:center; gap:8px;">
<label style="font-size:12px; width:50px; text-align:right;">邮箱:</label>
<input id="set-email" type="email" value="{{ Auth::user()->email ?? '' }}"
placeholder="用来找回密码(必填)"
style="flex:1; padding:5px; border:1px solid #ccc; border-radius:4px; font-size:12px;">
</div>
@if (\App\Models\SysParam::where('alias', 'smtp_enabled')->value('body') === '1')
<div style="display:flex; align-items:center; gap:8px; margin-top:6px;">
<label style="font-size:12px; width:50px; text-align:right;">验证码:</label>
<input id="set-email-code" type="text" placeholder="修改邮箱时必填" maxlength="6"
style="flex:1; padding:5px; border:1px solid #ccc; border-radius:4px; font-size:12px; max-width:100px;">
<button id="btn-send-code" type="button" onclick="sendEmailCode()"
style="padding:5px 10px; border:1px solid #336699; background:#eef5ff; color:#336699; border-radius:4px; font-size:12px; cursor:pointer;">
获取验证码
</button>
</div>
@endif
</div>
</div>
{{-- 密保设置 --}}
<div style="margin-bottom:16px; border:1px solid #e0e0e0; border-radius:6px; padding:12px;">
<div style="font-size:12px; font-weight:bold; color:#336699; margin-bottom:8px;">🛡️ 密码保护</div>
<div style="display:flex; flex-direction:column; gap:6px;">
<input id="set-question" type="text" value="{{ Auth::user()->question ?? '' }}"
placeholder="密保问题(如:我的小学名字?)"
style="padding:6px 8px; border:1px solid #ccc; border-radius:4px; font-size:12px;">
<input id="set-answer" type="text" value="{{ Auth::user()->answer ?? '' }}"
placeholder="密保答案"
style="padding:6px 8px; border:1px solid #ccc; border-radius:4px; font-size:12px;">
</div>
</div>
{{-- 保存按钮 --}}
<button onclick="saveSettings()"
style="width:100%; padding:8px; background:linear-gradient(135deg,#336699,#5a8fc0);
color:#fff; border:none; border-radius:4px; font-size:13px; font-weight:bold; cursor:pointer;">
💾 保存资料设置
</button>
</div>
</div>
</div>
{{-- ═══════════ 好友面板(独立文件)═══════════ --}}
@include('chat.partials.friend-panel')
{{-- ═══════════ 工具条相关 JS 函数 ═══════════ --}}
<script>
/**
* 保存密码(调用修改密码 API
*/
async function savePassword() {
const oldPwd = document.getElementById('set-old-pwd').value;
const newPwd = document.getElementById('set-new-pwd').value;
const newPwd2 = document.getElementById('set-new-pwd2').value;
if (!oldPwd || !newPwd) {
alert('请填写旧密码和新密码');
return;
}
if (newPwd.length < 6) {
alert('新密码最少6位');
return;
}
if (newPwd !== newPwd2) {
alert('两次输入的新密码不一致!');
return;
}
try {
const res = await fetch('{{ route('user.update_password') }}', {
method: 'PUT',
headers: {
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content,
'Content-Type': 'application/json',
'Accept': 'application/json'
},
body: JSON.stringify({
old_password: oldPwd,
new_password: newPwd,
new_password_confirmation: newPwd2
})
});
const data = await res.json();
if (res.ok && data.status === 'success') {
alert('密码修改成功!');
document.getElementById('set-old-pwd').value = '';
document.getElementById('set-new-pwd').value = '';
document.getElementById('set-new-pwd2').value = '';
} else {
alert('修改失败: ' + (data.message || '请输入正确的旧密码'));
}
} catch (e) {
alert('网络异常');
}
}
/**
* 保存个人资料和密保设置
*/
async function saveSettings() {
const profileData = {
sex: document.getElementById('set-sex').value,
email: document.getElementById('set-email').value,
email_code: document.getElementById('set-email-code') ? document.getElementById('set-email-code')
.value : '',
question: document.getElementById('set-question').value,
answer: document.getElementById('set-answer').value,
headface: @json(Auth::user()->usersf ?: '1.gif'),
sign: @json(Auth::user()->sign ?? '')
};
try {
const res = await fetch('{{ route('user.update_profile') }}', {
method: 'PUT',
headers: {
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content,
'Content-Type': 'application/json',
'Accept': 'application/json'
},
body: JSON.stringify(profileData)
});
const data = await res.json();
if (res.ok && data.status === 'success') {
alert('设置保存成功!');
} else {
alert('保存失败: ' + (data.message || '输入有误'));
}
} catch (e) {
alert('网络异常');
}
}
/**
* 发送邮箱验证码 (带有 60s 倒计时机制防灌水)
*/
async function sendEmailCode() {
const emailInput = document.getElementById('set-email').value.trim();
if (!emailInput) {
alert('请先填写邮箱地址后再获取验证码!');
return;
}
const btn = document.getElementById('btn-send-code');
btn.disabled = true;
btn.innerText = '正在发送...';
btn.style.opacity = '0.6';
btn.style.cursor = 'not-allowed';
try {
const res = await fetch('{{ route('user.send_email_code') }}', {
method: 'POST',
headers: {
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content,
'Content-Type': 'application/json',
'Accept': 'application/json'
},
body: JSON.stringify({
email: emailInput
})
});
const data = await res.json();
if (res.ok && data.status === 'success') {
alert(data.message || '验证码发送成功,请前往邮箱查收!(有效期5分钟)');
// 开始 60 秒防暴力点击倒计时
let count = 60;
btn.innerText = count + 's 后重试';
const timer = setInterval(() => {
count--;
if (count <= 0) {
clearInterval(timer);
btn.innerText = '获取验证码';
btn.disabled = false;
btn.style.opacity = '1';
btn.style.cursor = 'pointer';
} else {
btn.innerText = count + 's 后重试';
}
}, 1000);
} else {
alert('发送失败: ' + (data.message || '系统繁忙'));
// 失败了立刻解除禁用以重新尝试
btn.innerText = '获取验证码';
btn.disabled = false;
btn.style.opacity = '1';
btn.style.cursor = 'pointer';
}
} catch (e) {
alert('网络异常,验证码发送请求失败。');
btn.innerText = '获取验证码';
btn.disabled = false;
btn.style.opacity = '1';
btn.style.cursor = 'pointer';
}
}
</script>
{{-- ═══════════ 商店弹窗 ═══════════ --}}
<style>
/* 商店弹窗遮罩 */
#shop-modal {
display: none;
position: fixed;
inset: 0;
background: rgba(0, 0, 0, .5);
z-index: 9999;
justify-content: center;
align-items: center;
}
/* 弹窗主体 — 与设置弹窗同风格 */
#shop-modal-inner {
background: #fff;
border-radius: 8px;
width: 800px;
max-width: 95vw;
max-height: 84vh;
display: flex;
flex-direction: column;
box-shadow: 0 8px 32px rgba(0, 0, 0, .3);
overflow: hidden;
position: relative;
}
/* 标题栏 — 与设置/头像弹窗一致 */
#shop-modal-header {
background: linear-gradient(135deg, #336699, #5a8fc0);
color: #fff;
padding: 10px 16px;
display: flex;
align-items: center;
gap: 10px;
flex-shrink: 0;
}
#shop-modal-title {
font-size: 14px;
font-weight: bold;
flex: 1;
}
#shop-modal-jjb {
font-size: 12px;
color: #d0e8ff;
display: flex;
align-items: center;
gap: 3px;
background: rgba(0, 0, 0, .2);
padding: 2px 8px;
border-radius: 10px;
}
#shop-modal-jjb strong {
color: #ffe082;
font-size: 13px;
}
#shop-week-badge {
display: none;
font-size: 10px;
background: rgba(255, 255, 255, .2);
padding: 2px 7px;
border-radius: 10px;
color: #fff;
}
#shop-modal-close {
cursor: pointer;
font-size: 18px;
opacity: .8;
transition: opacity .15s;
line-height: 1;
}
#shop-modal-close:hover {
opacity: 1;
}
/* Toast */
#shop-toast {
display: none;
margin: 6px 12px 0;
padding: 5px 10px;
border-radius: 4px;
font-size: 12px;
font-weight: bold;
flex-shrink: 0;
}
/* 商品列表区 */
#shop-items-list {
flex: 1;
overflow-y: auto;
padding: 10px 12px;
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 8px;
align-content: start;
background: #f6faff;
}
/* 分组标题 — 独占一整行 */
.shop-group-header {
grid-column: 1 / -1;
font-size: 11px;
font-weight: bold;
color: #336699;
padding: 6px 4px 4px;
border-bottom: 1px solid #cde;
display: flex;
align-items: baseline;
gap: 6px;
}
.shop-group-header span {
font-size: 10px;
color: #888;
font-weight: normal;
}
/* 商品卡 */
.shop-card {
background: #fff;
border: 1px solid #d0e4f5;
border-radius: 6px;
padding: 10px;
display: flex;
flex-direction: column;
gap: 5px;
transition: border-color .2s, box-shadow .2s;
cursor: default;
}
.shop-card:hover {
border-color: #5a8fc0;
box-shadow: 0 2px 8px rgba(51, 102, 153, .18);
}
.shop-card-top {
display: flex;
align-items: center;
gap: 6px;
}
.shop-card-icon {
font-size: 20px;
flex-shrink: 0;
}
.shop-card-name {
font-size: 12px;
font-weight: bold;
color: #225588;
flex: 1;
line-height: 1.3;
}
.shop-card-desc {
font-size: 10px;
color: #888;
line-height: 1.4;
flex: 1;
}
.shop-btn {
display: flex;
align-items: center;
justify-content: center;
gap: 3px;
background: linear-gradient(135deg, #336699, #5a8fc0);
color: #fff;
border: none;
border-radius: 4px;
padding: 5px 8px;
cursor: pointer;
font-size: 11px;
font-weight: bold;
width: 100%;
margin-top: auto;
transition: opacity .15s;
}
.shop-btn:hover {
opacity: .85;
}
.shop-btn-use {
background: linear-gradient(135deg, #7c3aed, #9f67e8);
}
/* 改名内嵌遮罩 */
#shop-rename-overlay {
display: none;
position: absolute;
inset: 0;
background: rgba(0, 0, 0, .5);
z-index: 10001;
justify-content: center;
align-items: center;
border-radius: 8px;
}
#shop-rename-box {
background: #fff;
border: 1px solid #5a8fc0;
border-radius: 8px;
padding: 16px 14px;
width: 230px;
box-shadow: 0 4px 20px rgba(0, 0, 0, .25);
}
#shop-rename-box h4 {
font-size: 13px;
font-weight: bold;
color: #336699;
margin: 0 0 10px;
}
#rename-input {
width: 100%;
border: 1px solid #aac;
border-radius: 4px;
padding: 6px 8px;
color: #333;
font-size: 12px;
box-sizing: border-box;
margin-bottom: 8px;
outline: none;
}
#rename-input:focus {
border-color: #336699;
}
.rename-btn-row {
display: flex;
gap: 6px;
}
#rename-confirm {
flex: 1;
background: #336699;
color: #fff;
border: none;
border-radius: 4px;
padding: 6px;
cursor: pointer;
font-size: 12px;
font-weight: bold;
}
#rename-cancel {
flex: 1;
background: #eee;
color: #555;
border: 1px solid #ccc;
border-radius: 4px;
padding: 6px;
cursor: pointer;
font-size: 12px;
}
#rename-err {
color: #c00;
font-size: 10px;
margin-top: 5px;
min-height: 14px;
}
/* 送礼对话框 */
#gift-dialog {
display: none;
position: absolute;
inset: 0;
background: rgba(0, 0, 0, .55);
z-index: 10001;
justify-content: center;
align-items: center;
border-radius: 8px;
}
#gift-dialog-box {
background: #fff;
border: 1px solid #5a8fc0;
border-radius: 8px;
padding: 16px 14px;
width: 250px;
box-shadow: 0 4px 20px rgba(0, 0, 0, .25);
}
#gift-dialog-box h4 {
font-size: 12px;
font-weight: bold;
color: #336699;
margin: 0 0 3px;
}
#gift-item-name {
font-size: 11px;
color: #555;
display: block;
border-bottom: 1px dashed #cde;
padding-bottom: 6px;
margin-bottom: 10px;
}
.gift-label {
font-size: 11px;
color: #555;
margin-bottom: 3px;
display: block;
}
.gift-input {
width: 100%;
border: 1px solid #aac;
border-radius: 4px;
padding: 5px 8px;
font-size: 12px;
box-sizing: border-box;
margin-bottom: 8px;
outline: none;
}
.gift-input:focus {
border-color: #336699;
}
.gift-hint {
font-size: 10px;
color: #999;
margin: -4px 0 8px;
display: block;
}
#gift-err {
color: #c00;
font-size: 10px;
margin-top: 4px;
min-height: 14px;
}
</style>
<div id="shop-modal">
<div id="shop-modal-inner" style="position:relative;">
{{-- 标题栏 --}}
<div id="shop-modal-header">
<div id="shop-modal-title">🛍 道具商店</div>
<div id="shop-modal-jjb">🪙 <strong id="shop-jjb">--</strong> 金币</div>
<span id="shop-week-badge"></span>
<span id="shop-modal-close" onclick="closeShopModal()"></span>
</div>
{{-- Toast --}}
<div id="shop-toast"></div>
{{-- 商品网格 --}}
<div id="shop-items-list">
<div style="grid-column:1/-1; text-align:center; color:#6366f1; padding:30px 0; font-size:13px;">加载中…</div>
</div>
{{-- 改名内嵌遮罩 --}}
<div id="shop-rename-overlay">
<div id="shop-rename-box">
<h4>🎭 使用改名卡</h4>
<input id="rename-input" type="text" maxlength="10" placeholder="输入新昵称1-10字">
<div class="rename-btn-row">
<button id="rename-confirm" onclick="submitRename()">确认改名</button>
<button id="rename-cancel" onclick="closeRenameModal()">取消</button>
</div>
<div id="rename-err"></div>
</div>
</div>
{{-- 送礼对话框:填写接收人 + 赠言 --}}
<div id="gift-dialog">
<div id="gift-dialog-box">
<h4>🎁 赠出单次特效卡</h4>
<span id="gift-item-name"></span>
<label class="gift-label">送给谁?</label>
<input id="gift-recipient" class="gift-input" type="text" maxlength="20"
placeholder="用户名(留空 = 全场可见)">
<span class="gift-hint">💡 留空表示所有人;购买者必定可见</span>
<label class="gift-label">说一句话(公屏发送)</label>
<input id="gift-message" class="gift-input" type="text" maxlength="50" placeholder="可不填,百字以内">
<div id="gift-err"></div>
<div class="rename-btn-row" style="margin-top:8px;">
<button onclick="confirmGift()"
style="flex:1;background:#336699;color:#fff;border:none;border-radius:4px;padding:7px;cursor:pointer;font-size:12px;font-weight:bold;">确认赠出</button>
<button onclick="closeGiftDialog()"
style="flex:1;background:#eee;color:#555;border:1px solid #ccc;border-radius:4px;padding:7px;cursor:pointer;font-size:12px;">取消</button>
</div>
</div>
</div>
</div>
</div>
<script>
/**
* 商店弹窗逻辑
* 工具栏点击「商店」按钮触发,全屏模态展示
*/
(function() {
let shopLoaded = false;
/** 打开商店弹窗 */
window.openShopModal = function() {
document.getElementById('shop-modal').style.display = 'flex';
if (!shopLoaded) {
shopLoaded = true;
fetchShopData();
}
};
/** 关闭商店弹窗 */
window.closeShopModal = function() {
document.getElementById('shop-modal').style.display = 'none';
};
// 点击遮罩层关闭弹窗
document.getElementById('shop-modal').addEventListener('click', function(e) {
if (e.target === this) closeShopModal();
});
/** 拉取商品数据 */
function fetchShopData() {
fetch('{{ route('shop.items') }}', {
headers: {
'Accept': 'application/json',
'X-CSRF-TOKEN': _csrf()
}
})
.then(r => r.json())
.then(data => renderShop(data))
.catch(() => showShopToast('⚠ 加载失败,请重试', false));
}
/** 渲染商品列表2列网格 */
function renderShop(data) {
// 更新余额
document.getElementById('shop-jjb').textContent = Number(data.user_jjb).toLocaleString();
// 周卡状态
const badge = document.getElementById('shop-week-badge');
if (data.active_week_effect) {
const icons = {
fireworks: '🎆',
rain: '🌧',
lightning: '⚡',
snow: '❄️'
};
badge.textContent = (icons[data.active_week_effect] ?? '') + ' 周卡生效中';
badge.style.display = 'inline-block';
}
const ringCounts = data.ring_counts || {};
const groups = [{
label: '⚡ 单次特效卡',
desc: '立即播放一次,仅自己可见',
type: 'instant'
},
{
label: '📅 周卡 7天登录自动播放',
desc: '同时只能激活一种,购新旧失效不退款',
type: 'duration'
},
{
label: '💍 求婚戒指',
desc: '存入背包,求婚时消耗(被拒则遗失)',
type: 'ring'
},
{
label: '🎣 自动钓鱼卡',
desc: '激活后自动收篼,无需手动点击浮漂',
type: 'auto_fishing'
},
{
label: '🎭 道具',
desc: '',
type: 'one_time'
},
];
const list = document.getElementById('shop-items-list');
list.innerHTML = '';
groups.forEach(g => {
const items = data.items.filter(i => i.type === g.type);
if (!items.length) return;
// 分组标题(独占一整行)
const header = document.createElement('div');
header.className = 'shop-group-header';
// 分组标题徽章:
// - auto_fishing显示剩余时间紫色
// - duration显示当前已激活的周卡名称绿色
let groupSuffix = '';
if (g.type === 'auto_fishing' && (data.auto_fishing_minutes_left || 0) > 0) {
const left = data.auto_fishing_minutes_left;
const leftStr = left >= 60 ? Math.floor(left / 60) + ' 小时' : left + ' 分钟';
groupSuffix =
` <span style="display:inline-block;margin-left:8px;padding:1px 8px;background:#7c3aed;color:#fff;border-radius:10px;font-size:10px;font-weight:bold;vertical-align:middle;">⏳ 剩余 ${leftStr}</span>`;
} else if (g.type === 'duration' && data.active_week_effect) {
// active_week_effect 是 effectKey 字符串,从 items 列表反查对应商品名称
const effKey = data.active_week_effect;
const effItem = data.items.find(i => i.type === 'duration' && i.slug.includes(effKey));
const effName = effItem ? effItem.name : effKey;
groupSuffix =
` <span style="display:inline-block;margin-left:8px;padding:1px 8px;background:#16a34a;color:#fff;border-radius:10px;font-size:10px;font-weight:bold;vertical-align:middle;">✅ 已激活:${effName}</span>`;
}
header.innerHTML = `${g.label}${groupSuffix}${g.desc ? ` <span>${g.desc}</span>` : ''}`;
list.appendChild(header);
items.forEach(item => {
const isRename = item.slug === 'rename_card';
const canUse = isRename && data.has_rename_card;
const isRing = item.type === 'ring';
const ownedQty = isRing ? (ringCounts[item.id] || 0) : 0;
const card = document.createElement('div');
card.className = 'shop-card';
// 顶部:图标 + 名称(戒指加持有数徽章,其余正常显示)
const isAutoFishing = item.type === 'auto_fishing';
let iconHtml;
if (isRing && ownedQty > 0) {
iconHtml = `<span style="position:relative;display:inline-block;">
<span class="shop-card-icon">${item.icon}</span>
<span style="position:absolute;top:-4px;right:-6px;background:#f43f5e;color:#fff;font-size:9px;font-weight:800;min-width:15px;height:15px;border-radius:8px;text-align:center;line-height:15px;padding:0 2px;">${ownedQty}</span>
</span>`;
} else {
iconHtml = `<span class="shop-card-icon">${item.icon}</span>`;
}
const durationLabel = isAutoFishing && item.duration_minutes > 0 ?
`<div style="font-size:9px;margin-top:3px;color:#7c3aed;">⏱ 有效期 ${item.duration_minutes >= 60 ? Math.floor(item.duration_minutes / 60) + ' 小时' : item.duration_minutes + ' 分钟'}</div>` :
'';
card.innerHTML = `
<div class="shop-card-top">
${iconHtml}
<span class="shop-card-name">${item.name}</span>
</div>
<div class="shop-card-desc">${item.description ?? ''}</div>
${isRing && (item.intimacy_bonus > 0 || item.charm_bonus > 0) ? `
<div style="font-size:9px;margin-top:3px;display:flex;gap:8px;">
${item.intimacy_bonus > 0 ? `<span style="color:#f43f5e;">💞 亲密 +${item.intimacy_bonus}</span>` : ''}
${item.charm_bonus > 0 ? `<span style="color:#a855f7;">✨ 魅力 +${item.charm_bonus}</span>` : ''}
</div>` : ''}
${durationLabel}
`;
// 按钮
const btn = document.createElement('button');
if (canUse) {
btn.className = 'shop-btn shop-btn-use';
btn.textContent = '✦ 使用改名卡';
btn.onclick = openRenameModal;
} else if (item.type === 'instant') {
// 单次卡:打开送礼弹框
btn.className = 'shop-btn';
btn.innerHTML = `🪙 ${Number(item.price).toLocaleString()}`;
btn.onclick = () => openGiftDialog(item);
} else {
// 周卡、道具、戒指、自动钓鱼卡:弹确认窗口再购买
btn.className = 'shop-btn';
btn.innerHTML = `🪙 ${Number(item.price).toLocaleString()}`;
btn.onclick = async () => {
const confirmMsg =
`确认花费 🪙 ${Number(item.price).toLocaleString()} 金币购买\n【${item.name}】吗?`;
const ok = await window.chatDialog.confirm(confirmMsg, '确认购买');
if (ok) buyItem(item.id, item.name, item.price, 'all', '');
};
}
card.appendChild(btn);
list.appendChild(card);
});
});
}
/**
* 打开送礼弹框(仅单次特效卡)
* 让用户填写:送给谁 + 说一句话
*/
let _giftItem = null;
window.openGiftDialog = function(item) {
_giftItem = item;
// 重置内容
document.getElementById('gift-recipient').value = '';
document.getElementById('gift-message').value = '';
document.getElementById('gift-err').textContent = '';
document.getElementById('gift-item-name').textContent =
`${item.icon} ${item.name}(🪙 ${Number(item.price).toLocaleString()}`;
document.getElementById('gift-dialog').style.display = 'flex';
};
window.closeGiftDialog = function() {
document.getElementById('gift-dialog').style.display = 'none';
_giftItem = null;
};
/** 送礼确认,提交购买 */
window.confirmGift = function() {
if (!_giftItem) return;
const recipient = document.getElementById('gift-recipient').value.trim();
const message = document.getElementById('gift-message').value.trim();
document.getElementById('gift-err').textContent = '';
// 先将商品数据保存到局部变量再关闭弹框closeGiftDialog 会把 _giftItem 设为 null
const item = _giftItem;
closeGiftDialog();
buyItem(item.id, item.name, item.price, recipient, message);
};
/** 购买商品(最终执行) */
window.buyItem = function(itemId, name, price, recipient, message) {
const roomId = window.chatContext?.roomId ?? 0;
fetch('{{ route('shop.buy') }}', {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
'X-CSRF-TOKEN': _csrf()
},
body: JSON.stringify({
item_id: itemId,
recipient: recipient || 'all',
message: message || '',
room_id: roomId,
}),
})
.then(r => r.json())
.then(data => {
if (data.status === 'success') {
// 更新金币
if (data.jjb !== undefined)
document.getElementById('shop-jjb').textContent = Number(data.jjb)
.toLocaleString();
// 购买成功提示
showShopToast(`✅ ${name} 购买成功!`, true);
// 播放本地特效(购买者自己必须也能看到)
if (data.play_effect && window.EffectManager) {
window.EffectManager.play(data.play_effect);
}
// 延迟刷新商品数据(保持商店开着让用户看到状态更新)
shopLoaded = false;
setTimeout(() => {
fetchShopData();
shopLoaded = true;
}, 1000);
} else {
showShopToast(data.message, false);
}
})
.catch(() => showShopToast('⚠ 网络异常,请重试', false));
};
/** Toast 通知 */
window.showShopToast = function(msg, ok) {
const el = document.getElementById('shop-toast');
el.textContent = msg;
el.style.display = 'block';
el.style.background = ok ? '#064e3b' : '#7f1d1d';
el.style.color = ok ? '#6ee7b7' : '#fca5a5';
clearTimeout(el._t);
el._t = setTimeout(() => {
el.style.display = 'none';
}, 3500);
};
/** 打开改名框 */
window.openRenameModal = function() {
const m = document.getElementById('shop-rename-overlay');
m.style.display = 'flex';
document.getElementById('rename-input').focus();
document.getElementById('rename-err').textContent = '';
};
/** 关闭改名框 */
window.closeRenameModal = function() {
document.getElementById('shop-rename-overlay').style.display = 'none';
};
/** 提交改名 */
window.submitRename = function() {
const newName = document.getElementById('rename-input').value.trim();
if (!newName) {
document.getElementById('rename-err').textContent = '请输入新昵称';
return;
}
fetch('{{ route('shop.rename') }}', {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
'X-CSRF-TOKEN': _csrf()
},
body: JSON.stringify({
new_name: newName
}),
})
.then(r => r.json())
.then(data => {
if (data.status === 'success') {
closeRenameModal();
showShopToast(data.message, true);
shopLoaded = false;
setTimeout(() => window.location.reload(), 2000);
} else {
document.getElementById('rename-err').textContent = data.message;
}
})
.catch(() => {
document.getElementById('rename-err').textContent = '网络异常,请重试';
});
};
function _csrf() {
return document.querySelector('meta[name="csrf-token"]')?.content ?? '';
}
})();
</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>