功能:管理员全员清屏 + 离开提示趣味风格
- 新增 ScreenCleared 广播事件
- AdminCommandController 添加 clearScreen 方法(站长权限)
- ChatStateService 添加 clearMessages 方法
- chat.js 添加 ScreenCleared Echo 监听
- 前端:全员清屏按钮(红色🧹)+ 清屏处理逻辑(保留悄悄话)
- 离开提示改为与进入一致的趣味随机语风格(橙色【离开】标签)
This commit is contained in:
59
app/Events/ScreenCleared.php
Normal file
59
app/Events/ScreenCleared.php
Normal file
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* 文件功能:管理员全员清屏广播事件
|
||||
*
|
||||
* 管理员触发清屏后,广播给房间内所有用户,前端监听后清除聊天记录(悄悄话除外)。
|
||||
*
|
||||
* @author ChatRoom Laravel
|
||||
*
|
||||
* @version 1.0.0
|
||||
*/
|
||||
|
||||
namespace App\Events;
|
||||
|
||||
use Illuminate\Broadcasting\InteractsWithSockets;
|
||||
use Illuminate\Broadcasting\PresenceChannel;
|
||||
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
|
||||
use Illuminate\Foundation\Events\Dispatchable;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class ScreenCleared implements ShouldBroadcast
|
||||
{
|
||||
use Dispatchable, InteractsWithSockets, SerializesModels;
|
||||
|
||||
/**
|
||||
* 构造函数
|
||||
*
|
||||
* @param int $roomId 房间ID
|
||||
* @param string $operator 执行清屏的管理员用户名
|
||||
*/
|
||||
public function __construct(
|
||||
public readonly int $roomId,
|
||||
public readonly string $operator,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* 广播频道
|
||||
*
|
||||
* @return array<int, \Illuminate\Broadcasting\Channel>
|
||||
*/
|
||||
public function broadcastOn(): array
|
||||
{
|
||||
return [
|
||||
new PresenceChannel('room.'.$this->roomId),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 广播数据
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function broadcastWith(): array
|
||||
{
|
||||
return [
|
||||
'operator' => $this->operator,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -323,6 +323,39 @@ class AdminCommandController extends Controller
|
||||
return response()->json(['status' => 'success', 'message' => '公告已发送']);
|
||||
}
|
||||
|
||||
/**
|
||||
* 管理员全员清屏
|
||||
*
|
||||
* 清除 Redis 中该房间的聊天记录缓存,并广播清屏事件通知所有用户前端清除消息。
|
||||
* 前端只清除普通消息,保留悄悄话。
|
||||
*
|
||||
* @param Request $request 请求对象,需包含 room_id
|
||||
* @return JsonResponse 操作结果
|
||||
*/
|
||||
public function clearScreen(Request $request): JsonResponse
|
||||
{
|
||||
$request->validate([
|
||||
'room_id' => 'required|integer',
|
||||
]);
|
||||
|
||||
$admin = Auth::user();
|
||||
$roomId = $request->input('room_id');
|
||||
$superLevel = (int) Sysparam::getValue('superlevel', '100');
|
||||
|
||||
// 需要站长权限才能全员清屏
|
||||
if ($admin->user_level < $superLevel) {
|
||||
return response()->json(['status' => 'error', 'message' => '仅站长可执行全员清屏'], 403);
|
||||
}
|
||||
|
||||
// 清除 Redis 中该房间的消息缓存
|
||||
$this->chatState->clearMessages($roomId);
|
||||
|
||||
// 广播清屏事件
|
||||
broadcast(new \App\Events\ScreenCleared($roomId, $admin->username));
|
||||
|
||||
return response()->json(['status' => 'success', 'message' => '已执行全员清屏']);
|
||||
}
|
||||
|
||||
/**
|
||||
* 权限检查:管理员是否可对目标用户执行指定操作
|
||||
*
|
||||
|
||||
@@ -104,6 +104,17 @@ class ChatStateService
|
||||
Redis::ltrim($key, -$maxKeep, -1);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除指定房间的所有消息缓存(管理员全员清屏)。
|
||||
*
|
||||
* @param int $roomId 房间ID
|
||||
*/
|
||||
public function clearMessages(int $roomId): void
|
||||
{
|
||||
$key = "room:{$roomId}:messages";
|
||||
Redis::del($key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定房间的新发言记录。
|
||||
* 在高频长轮询或前端断线重连拉取时使用。
|
||||
|
||||
@@ -56,6 +56,13 @@ export function initChat(roomId) {
|
||||
window.dispatchEvent(
|
||||
new CustomEvent("chat:title-updated", { detail: e }),
|
||||
);
|
||||
})
|
||||
// 监听管理员全员清屏
|
||||
.listen("ScreenCleared", (e) => {
|
||||
console.log("全员清屏:", e);
|
||||
window.dispatchEvent(
|
||||
new CustomEvent("chat:screen-cleared", { detail: e }),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -95,6 +95,9 @@
|
||||
<button type="button" onclick="promptAnnounceMessage()"
|
||||
style="font-size: 11px; padding: 1px 6px; background: #7c3aed; color: #fff; border: none; border-radius: 2px; cursor: pointer;">📢
|
||||
公屏</button>
|
||||
<button type="button" onclick="adminClearScreen()"
|
||||
style="font-size: 11px; padding: 1px 6px; background: #dc2626; color: #fff; border: none; border-radius: 2px; cursor: pointer;">🧹
|
||||
清屏</button>
|
||||
@endif
|
||||
|
||||
<button type="button" onclick="location.reload()"
|
||||
|
||||
@@ -400,15 +400,37 @@
|
||||
delete onlineUsers[user.username];
|
||||
renderUserList();
|
||||
|
||||
// 原版风格:趣味离开语(与进入一致的风格)
|
||||
const gender = user.sex == 2 ? '美女' : '帅哥';
|
||||
const uname = user.username;
|
||||
const leaveTemplates = [
|
||||
`${gender}<b>${uname}</b>潇洒地挥了挥手,骑着小毛驴哼着小调离去了`,
|
||||
`${gender}<b>${uname}</b>开着跑车扬长而去,留下一路烟尘`,
|
||||
`${gender}<b>${uname}</b>踩着七彩祥云飘然远去,消失在天际`,
|
||||
`${gender}<b>${uname}</b>悄无声息地溜走了,连个招呼都不打`,
|
||||
`${gender}<b>${uname}</b>跳上直升机螺旋桨呼呼作响,朝大家喊道:"我先走啦!"`,
|
||||
`${gender}<b>${uname}</b>拱手告别:"各位大虾,后会有期!"随后翩然离去`,
|
||||
`${gender}<b>${uname}</b>骑着自行车铃铛叮当响,远远就喊:"下次再聊!拜拜!"`,
|
||||
`${gender}<b>${uname}</b>坐着热气球缓缓升空,朝大家挥手告别`,
|
||||
`${gender}<b>${uname}</b>迈着六亲不认的步伐离开了,留下一众人目瞪口呆`,
|
||||
`${gender}<b>${uname}</b>化作一缕青烟消散在空气中……`,
|
||||
];
|
||||
const msg = leaveTemplates[Math.floor(Math.random() * leaveTemplates.length)];
|
||||
const now = new Date();
|
||||
const timeStr = now.getHours().toString().padStart(2, '0') + ':' +
|
||||
now.getMinutes().toString().padStart(2, '0') + ':' +
|
||||
now.getSeconds().toString().padStart(2, '0');
|
||||
|
||||
const sysDiv = document.createElement('div');
|
||||
sysDiv.className = 'msg-line sys-msg';
|
||||
// VIP 用户离开也带专属颜色
|
||||
sysDiv.className = 'msg-line';
|
||||
// VIP 用户离开也带专属颜色和图标
|
||||
if (user.vip_icon && user.vip_name) {
|
||||
const vipColor = user.vip_color || '#f59e0b';
|
||||
sysDiv.innerHTML =
|
||||
`<span style="color: ${vipColor}">☆ ${user.vip_icon}${user.vip_name} ${user.username} 潇洒离去 ☆</span>`;
|
||||
`<span style="color: ${vipColor}; font-weight: bold;">【${user.vip_icon} ${user.vip_name}】${msg}</span><span class="msg-time">(${timeStr})</span>`;
|
||||
} else {
|
||||
sysDiv.innerHTML = `<span style="color: gray">☆ ${user.username} 离开了聊天室 ☆</span>`;
|
||||
sysDiv.innerHTML =
|
||||
`<span style="color: #cc6600">【离开】${msg}</span><span class="msg-time">(${timeStr})</span>`;
|
||||
}
|
||||
container.appendChild(sysDiv);
|
||||
scrollToBottom();
|
||||
@@ -475,6 +497,41 @@
|
||||
document.getElementById('room-title-display').innerText = e.detail.title;
|
||||
});
|
||||
|
||||
// ── 管理员全员清屏事件 ───────────────────────
|
||||
window.addEventListener('chat:screen-cleared', (e) => {
|
||||
const operator = e.detail.operator;
|
||||
|
||||
// 清除公聊窗口(say1)所有消息
|
||||
const say1 = document.getElementById('say');
|
||||
if (say1) say1.innerHTML = '';
|
||||
|
||||
// 清除包厢窗口(say2)中非悄悄话的消息
|
||||
const say2 = document.getElementById('say2');
|
||||
if (say2) {
|
||||
const items = say2.querySelectorAll('.msg-line');
|
||||
items.forEach(item => {
|
||||
// 保留悄悄话消息(含 msg-secret 类)
|
||||
if (!item.querySelector('.msg-secret')) {
|
||||
item.remove();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 显示清屏提示
|
||||
const sysDiv = document.createElement('div');
|
||||
sysDiv.className = 'msg-line';
|
||||
const now = new Date();
|
||||
const timeStr = now.getHours().toString().padStart(2, '0') + ':' +
|
||||
now.getMinutes().toString().padStart(2, '0') + ':' +
|
||||
now.getSeconds().toString().padStart(2, '0');
|
||||
sysDiv.innerHTML =
|
||||
`<span style="color: #dc2626; font-weight: bold;">🧹 管理员 <b>${operator}</b> 已执行全员清屏</span><span class="msg-time">(${timeStr})</span>`;
|
||||
if (say1) {
|
||||
say1.appendChild(sysDiv);
|
||||
say1.scrollTop = say1.scrollHeight;
|
||||
}
|
||||
});
|
||||
|
||||
// ── 发送消息(Enter 发送) ───────────────────────
|
||||
document.getElementById('content').addEventListener('keydown', function(e) {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
@@ -670,6 +727,32 @@
|
||||
}
|
||||
}
|
||||
|
||||
// ── 管理员全员清屏 ─────────────────────────────────────
|
||||
async function adminClearScreen() {
|
||||
if (!confirm('确定要清除所有人的聊天记录吗?(悄悄话将保留)')) return;
|
||||
|
||||
try {
|
||||
const res = await fetch('/command/clear-screen', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').getAttribute(
|
||||
'content'),
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
room_id: window.chatContext.roomId,
|
||||
})
|
||||
});
|
||||
const data = await res.json();
|
||||
if (!res.ok || data.status !== 'success') {
|
||||
alert(data.message || '清屏失败');
|
||||
}
|
||||
} catch (e) {
|
||||
alert('清屏失败:' + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
// ── 滚屏开关 ─────────────────────────────────────
|
||||
function toggleAutoScroll() {
|
||||
autoScroll = !autoScroll;
|
||||
|
||||
@@ -93,6 +93,7 @@ Route::middleware(['chat.auth'])->group(function () {
|
||||
Route::post('/command/freeze', [AdminCommandController::class, 'freeze'])->name('command.freeze');
|
||||
Route::get('/command/whispers/{username}', [AdminCommandController::class, 'viewWhispers'])->name('command.whispers');
|
||||
Route::post('/command/announce', [AdminCommandController::class, 'announce'])->name('command.announce');
|
||||
Route::post('/command/clear-screen', [AdminCommandController::class, 'clearScreen'])->name('command.clear_screen');
|
||||
});
|
||||
|
||||
// 强力特权层中间件:同时验证 chat.auth 登录态 和 chat.level:super 特权(superlevel 由 sysparam 配置)
|
||||
|
||||
Reference in New Issue
Block a user