Files
chatroom/resources/views/chat/partials/toolbar.blade.php
lkddi e2ae4b34b3 修复:Chrome 离开按钮 confirm 弹窗闪烁 → 自定义 HTML 弹窗
- 移除原生 confirm(),改为自定义 #leave-confirm-modal 弹窗
- 红色渐变标题栏,取消/确定离开两个按钮
- 点击遮罩可关闭,不触发任何浏览器原生对话框机制
2026-03-01 00:29:00 +08:00

1028 lines
38 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="alert('🚧 娱乐功能开发中,敬请期待!')" title="娱乐(待开发)">娱乐</div>
<div class="tool-btn" onclick="alert('🚧 银行功能开发中,敬请期待!')" title="银行(待开发)">银行</div>
<div class="tool-btn" onclick="alert('🚧 呼叫功能开发中,敬请期待!')" 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="alert('🚧 提议功能开发中,敬请期待!')" 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="showLeaveConfirm()" title="离开聊天室">离开
</div>
</div>
{{-- ═══════════ 离开确认弹窗(自定义,避免 Chrome 原生 confirm 冲突)═══════════ --}}
<div id="leave-confirm-modal"
style="display:none; position:fixed; inset:0; background:rgba(0,0,0,.5); z-index:99999;
justify-content:center; align-items:center;">
<div
style="background:#fff; border-radius:8px; width:300px; box-shadow:0 8px 32px rgba(0,0,0,.3);
overflow:hidden;">
<div
style="background:linear-gradient(135deg,#cc4444,#e06060); color:#fff;
padding:12px 16px; font-size:14px; font-weight:bold;">
⚠️ 离开聊天室</div>
<div style="padding:20px 16px; font-size:13px; color:#444; text-align:center;">
确定要离开聊天室吗?
</div>
<div style="display:flex; gap:10px; padding:0 16px 16px;">
<button onclick="hideLeaveConfirm()"
style="flex:1; padding:8px; background:#eee; color:#555; border:1px solid #ccc;
border-radius:4px; font-size:13px; cursor:pointer;">取消</button>
<button onclick="hideLeaveConfirm(); leaveRoom();"
style="flex:1; padding:8px; background:#cc4444; color:#fff; border:none;
border-radius:4px; font-size:13px; font-weight:bold; cursor:pointer;">确定离开</button>
</div>
</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>
{{-- ═══════════ 工具条相关 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';
}
}
/** 显示离开确认弹窗(替代原生 confirm避免 Chrome 弹窗冲突) */
function showLeaveConfirm() {
document.getElementById('leave-confirm-modal').style.display = 'flex';
}
/** 隐藏离开确认弹窗 */
function hideLeaveConfirm() {
document.getElementById('leave-confirm-modal').style.display = 'none';
}
// 点遮罩关闭
document.getElementById('leave-confirm-modal').addEventListener('click', function(e) {
if (e.target === this) hideLeaveConfirm();
});
</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: 520px;
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: 1fr 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 groups = [{
label: '⚡ 单次特效卡',
desc: '立即播放一次,仅自己可见',
type: 'instant'
},
{
label: '📅 周卡 7天登录自动播放',
desc: '同时只能激活一种,购新旧失效不退款',
type: 'duration'
},
{
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';
header.innerHTML = `${g.label}${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 card = document.createElement('div');
card.className = 'shop-card';
// 顶部:图标 + 名称
card.innerHTML = `
<div class="shop-card-top">
<span class="shop-card-icon">${item.icon}</span>
<span class="shop-card-name">${item.name}</span>
</div>
<div class="shop-card-desc">${item.description ?? ''}</div>
`;
// 按钮
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 = () => 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();
// 播放本地特效(购买者自己必须也能看到)
if (data.play_effect && window.EffectManager) {
window.EffectManager.play(data.play_effect);
}
// 关闭商店弹窗,让用户欣赏特效
closeShopModal();
// 延迟刷新商品数据
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>