重构(chat): 聊天室 Partials 第二阶段分类拆分及修复红包弹窗隐藏 Bug

- 完成对 scripts.blade.php 中非核心业务逻辑(钓鱼游戏、AI机器人、系统全局公告)的深度抽象隔离
- 修复抢红包逻辑中 setInterval 缺失时间参数(1000)引发浏览器前端主线程挂起的重度阻塞问题
- 修复 lottery-panel 组件结尾漏写 </div> 导致的连锁级渲染树崩溃(该崩溃导致红包节点被意外当作隐藏后代节点渲染,造成彻底不可见)
- 对相关模板规范代码结构,执行 Laravel Pint 格式化并提交
This commit is contained in:
2026-03-09 11:30:11 +08:00
parent 28d9f9ee96
commit bfb1a3bca4
24 changed files with 2806 additions and 2601 deletions
@@ -121,6 +121,7 @@
// 自定义弹窗:直接代理到全局 window.chatDialog
$alert: (...args) => window.chatDialog.alert(...args),
$confirm: (...args) => window.chatDialog.confirm(...args),
$prompt: (...args) => window.chatDialog.prompt(...args),
/** 切换好友关系(加好友 / 删好友) */
async toggleFriend() {
@@ -354,7 +355,7 @@
},
/** 踢出用户 */
async kickUser() {
const reason = prompt('踢出原因(可留空):', '违反聊天室规则');
const reason = await this.$prompt('踢出原因(可留空):', '违反聊天室规则', '踢出用户', '#cc4444');
if (reason === null) return;
try {
const res = await fetch('/command/kick', {
@@ -370,10 +371,10 @@
if (data.status === 'success') {
this.showUserModal = false;
} else {
alert('操作失败:' + data.message);
this.$alert('操作失败:' + data.message, '操作失败', '#cc4444');
}
} catch (e) {
alert('网络异常');
this.$alert('网络异常', '错误', '#cc4444');
}
},
@@ -393,16 +394,16 @@
if (data.status === 'success') {
this.showUserModal = false;
} else {
alert('操作失败:' + data.message);
this.$alert('操作失败:' + data.message, '操作失败', '#cc4444');
}
} catch (e) {
alert('网络异常');
this.$alert('网络异常', '错误', '#cc4444');
}
},
/** 警告用户 */
async warnUser() {
const reason = prompt('警告原因:', '请注意言行');
const reason = await this.$prompt('警告原因:', '请注意言行', '警告用户', '#f59e0b');
if (reason === null) return;
try {
const res = await fetch('/command/warn', {
@@ -418,17 +419,22 @@
if (data.status === 'success') {
this.showUserModal = false;
} else {
alert('操作失败:' + data.message);
this.$alert('操作失败:' + data.message, '操作失败', '#cc4444');
}
} catch (e) {
alert('网络异常');
this.$alert('网络异常', '错误', '#cc4444');
}
},
/** 冻结用户 */
async freezeUser() {
if (!confirm('确定要冻结 ' + this.userInfo.username + ' 的账号吗?冻结后将无法登录!')) return;
const reason = prompt('冻结原因:', '严重违规');
const confirmed = await this.$confirm(
'确定要冻结 ' + this.userInfo.username + ' 的账号吗?冻结后将无法登录!',
'冻结账号',
'#cc4444'
);
if (!confirmed) return;
const reason = await this.$prompt('冻结原因:', '严重违规', '填写原因', '#cc4444');
if (reason === null) return;
try {
const res = await fetch('/command/freeze', {
@@ -444,10 +450,10 @@
if (data.status === 'success') {
this.showUserModal = false;
} else {
alert('操作失败:' + data.message);
this.$alert('操作失败:' + data.message, '操作失败', '#cc4444');
}
} catch (e) {
alert('网络异常');
this.$alert('网络异常', '错误', '#cc4444');
}
},
@@ -464,10 +470,10 @@
this.whisperList = data.messages;
this.showWhispers = true;
} else {
alert(data.message);
this.$alert(data.message, '操作失败', '#cc4444');
}
} catch (e) {
alert('网络异常');
this.$alert('网络异常', '错误', '#cc4444');
}
},
@@ -488,10 +494,10 @@
this.announceText = '';
this.showAnnounce = false;
} else {
alert(data.message);
this.$alert(data.message, '操作失败', '#cc4444');
}
} catch (e) {
alert('网络异常');
this.$alert('网络异常', '错误', '#cc4444');
}
},
@@ -1010,10 +1016,10 @@
async send() {
if (this.sending) return;
const amt = parseInt(this.amount, 10);
if (!amt || amt <= 0) { alert('请输入有效金额'); return; }
if (!amt || amt <= 0) { window.chatDialog.alert('请输入有效金额', '提示', '#f59e0b'); return; }
const maxOnce = window.chatContext?.myMaxReward;
if (maxOnce === 0) { alert('你的职务没有奖励发放权限'); return; }
if (maxOnce > 0 && amt > maxOnce) { alert('超出单次上限 ' + maxOnce + ' 金币'); return; }
if (maxOnce === 0) { window.chatDialog.alert('你的职务没有奖励发放权限', '无权限', '#cc4444'); return; }
if (maxOnce > 0 && amt > maxOnce) { window.chatDialog.alert('超出单次上限 ' + maxOnce + ' 金币', '超出上限', '#cc4444'); return; }
this.sending = true;
try {
const res = await fetch(window.chatContext.rewardUrl, {
@@ -1040,18 +1046,18 @@
this.quota.recent_rewards.unshift({ target: this.targetUsername, amount: amt, created_at: mm + '-' + dd + ' ' + hh + ':' + mi });
if (this.quota.recent_rewards.length > 10) this.quota.recent_rewards.pop();
this.amount = '';
alert(data.message);
window.chatDialog.alert(data.message, '🎉 奖励发放成功', '#d97706');
} else {
alert(data.message || '发放失败');
window.chatDialog.alert(data.message || '发放失败', '操作失败', '#cc4444');
}
} catch { alert('网络异常,请稍后重试'); }
} catch { window.chatDialog.alert('网络异常,请稍后重试', '错误', '#cc4444'); }
this.sending = false;
}
}">
<div x-show="show" style="display:none; position:fixed; inset:0; background:rgba(0,0,0,.55); z-index:9900;"
x-on:click.self="show = false">
<div x-show="show"
style="position:absolute; top:50%; left:50%; transform:translate(-50%,-50%);
style="display:none; position:absolute; top:50%; left:50%; transform:translate(-50%,-50%);
width:520px; max-width:95vw; background:#fff; border-radius:16px;
box-shadow:0 20px 60px rgba(0,0,0,.25); overflow:hidden;">
{{-- 标题栏 --}}
@@ -1162,7 +1168,207 @@
const el = document.getElementById('reward-modal-container');
if (el) {
const data = Alpine.$data(el);
if (data) data.open(username);
if (data) {
data.open(username);
}
}
}
</script>
{{-- ═══════════ 好友系统通知监听 ═══════════ --}}
{{-- 监听好友 WebSocket 事件,与好友操作逻辑集中在同一文件维护 --}}
<script>
// ── 好友系统私有频道监听(仅本人可见) ────────────────
/**
* 监听当前用户的私有频道 `user.{id}`
* 收到 FriendAdded / FriendRemoved 事件时用弹窗通知。
* FriendAdded 居中大卡弹窗(chatBanner 风格)
* FriendRemoved 右下角 Toast 通知
*/
function setupFriendNotification() {
if (!window.Echo || !window.chatContext) {
setTimeout(setupFriendNotification, 500);
return;
}
const myId = window.chatContext.userId;
window.Echo.private(`user.${myId}`)
.listen('.FriendAdded', (e) => {
showFriendBanner(e.from_username, e.has_added_back);
})
.listen('.FriendRemoved', (e) => {
if (e.had_added_back) {
window.chatToast.show({
title: '好友通知',
message: `<b>${e.from_username}</b> 已将你从好友列表移除。<br><span style="color:#6b7280; font-size:12px;">你的好友列表中仍保留对方,可点击同步移除。</span>`,
icon: '👥',
color: '#6b7280',
duration: 10000,
action: {
label: `🗑️ 同步移除 ${e.from_username}`,
onClick: async () => {
const url = `/friend/${encodeURIComponent(e.from_username)}/remove`;
const csrf = document.querySelector('meta[name="csrf-token"]')
?.content ?? '';
await fetch(url, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': csrf,
'Accept': 'application/json'
},
body: JSON.stringify({
room_id: window.chatContext?.roomId
}),
});
}
},
});
} else {
window.chatToast.show({
title: '好友通知',
message: `<b>${e.from_username}</b> 已将你从他的好友列表移除。`,
icon: '👥',
color: '#9ca3af',
});
}
});
}
document.addEventListener('DOMContentLoaded', setupFriendNotification);
// ── BannerNotification:通用大卡片通知监听 ──────────────────
/**
* 监听 BannerNotification 事件,渲染 chatBanner 大卡片。
* 支持私有用户频道(单推)和房间频道(全员推送)。
*
* 安全说明:BannerNotification 仅由后端可信代码 broadcast
* 私有频道需鉴权,presence 频道需加入房间,均须服务端验证身份。
*/
function setupBannerNotification() {
if (!window.Echo || !window.chatContext) {
setTimeout(setupBannerNotification, 500);
return;
}
const myId = window.chatContext.userId;
const roomId = window.chatContext.roomId;
// 监听私有用户频道(单独推给某人)
window.Echo.private(`user.${myId}`)
.listen('.BannerNotification', (e) => {
if (e.options && typeof e.options === 'object') {
window.chatBanner.show(e.options);
}
});
// 监听房间频道(推给房间所有人)
if (roomId) {
window.Echo.join(`room.${roomId}`)
.listen('.BannerNotification', (e) => {
if (e.options && typeof e.options === 'object') {
window.chatBanner.show(e.options);
}
});
}
}
document.addEventListener('DOMContentLoaded', setupBannerNotification);
/**
* 显示好友添加居中大卡弹窗(使用 chatBanner 公共组件)。
* 互相好友 绿色渐变 + 互为好友文案
* 单向添加 蓝绿渐变 + 提示回加 + [ 回加好友] 按钮
*
* @param {string} fromUsername 添加者用户名
* @param {boolean} hasAddedBack 接收方是否已将添加者加为好友
*/
function showFriendBanner(fromUsername, hasAddedBack) {
if (hasAddedBack) {
window.chatBanner.show({
id: 'friend-banner',
icon: '🎉💚🎉',
title: '好友通知',
name: fromUsername,
body: '将你加为好友了!',
sub: '<strong style="color:#a7f3d0;">你们现在互为好友 🎊</strong>',
gradient: ['#065f46', '#059669', '#10b981'],
titleColor: '#a7f3d0',
autoClose: 5000,
});
} else {
window.chatBanner.show({
id: 'friend-banner',
icon: '💚📩',
title: '好友申请',
name: fromUsername,
body: '将你加为好友了!',
sub: '但你还没有回加对方为好友',
gradient: ['#1e3a5f', '#1d4ed8', '#0891b2'],
titleColor: '#bae6fd',
autoClose: 0,
buttons: [{
label: ' 回加好友',
color: '#10b981',
onClick: async (btn, close) => {
await quickFriendAction('add', fromUsername, btn);
if (btn.textContent.startsWith('✅')) {
setTimeout(close, 1500);
}
},
},
{
label: '稍后再说',
color: 'rgba(255,255,255,0.15)',
onClick: (btn, close) => close(),
},
],
});
}
}
/**
* 聊天区悄悄话内嵌链接的快捷好友操作。
* 由后端生成的 onclick="quickFriendAction('add'/'remove', username, this)" 调用。
*
* @param {string} act 'add' | 'remove'
* @param {string} username 目标用户名
* @param {HTMLElement} el 被点击的 <a> 元素,用于更新显示状态
*/
window.quickFriendAction = async function(act, username, el) {
if (el.dataset.done) {
return;
}
el.dataset.done = '1';
el.textContent = '处理中…';
el.style.pointerEvents = 'none';
try {
const method = act === 'add' ? 'POST' : 'DELETE';
const url = `/friend/${encodeURIComponent(username)}/${act === 'add' ? 'add' : 'remove'}`;
const csrf = document.querySelector('meta[name="csrf-token"]')?.content ?? '';
const res = await fetch(url, {
method,
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': csrf,
'Accept': 'application/json',
},
body: JSON.stringify({
room_id: window.chatContext?.roomId
}),
});
const data = await res.json();
if (data.status === 'success') {
el.textContent = act === 'add' ? '✅ 已回加' : '✅ 已移除';
el.style.color = '#16a34a';
el.style.textDecoration = 'none';
} else {
el.textContent = '❌ ' + (data.message || '操作失败');
el.style.color = '#cc4444';
}
} catch (e) {
el.textContent = '❌ 网络错误';
el.style.color = '#cc4444';
delete el.dataset.done;
el.style.pointerEvents = '';
}
};
</script>