重构(chat): 聊天室 Partials 第二阶段分类拆分及修复红包弹窗隐藏 Bug
- 完成对 scripts.blade.php 中非核心业务逻辑(钓鱼游戏、AI机器人、系统全局公告)的深度抽象隔离 - 修复抢红包逻辑中 setInterval 缺失时间参数(1000)引发浏览器前端主线程挂起的重度阻塞问题 - 修复 lottery-panel 组件结尾漏写 </div> 导致的连锁级渲染树崩溃(该崩溃导致红包节点被意外当作隐藏后代节点渲染,造成彻底不可见) - 对相关模板规范代码结构,执行 Laravel Pint 格式化并提交
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user